Coleta de lixo ao usar representantes anônimos para manipulação de events

ATUALIZAR

Eu combinei várias respostas daqui em uma resposta ‘definitiva’ em uma nova questão .

Pergunta original

No meu código eu tenho um editor de events, que existe por toda a vida útil do aplicativo (aqui reduzido ao essencial):

public class Publisher { //ValueEventArgs inherits from EventArgs public event EventHandler<ValueEventArgs> EnabledChanged; } 

Como esse editor pode ser usado em todo o lugar, fiquei muito satisfeito comigo mesmo por criar essa pequena class auxiliar para evitar rewrite o código de manipulação em todos os assinantes:

 public static class Linker { public static void Link(Publisher publisher, Control subscriber) { publisher.EnabledChanged += (s, e) => subscriber.Enabled = e.Value; } //(Non-lambda version, if you're not comfortable with lambdas) public static void Link(Publisher publisher, Control subscriber) { publisher.EnabledChanged += delegate(object sender, ValueEventArgs e) { subscriber.Enabled = e.Value; }; } } 

Funcionou bem, até que começamos a usá-lo em máquinas menores, quando comecei a receber o ocasional:

 System.ComponentModel.Win32Exception Not enough storage is available to process this command 

Como se constata, há um lugar no código em que os controles dos assinantes estão sendo criados dinamicamente, adicionados e removidos de um formulário. Dado o meu conhecimento avançado de garbage collection, etc (ou seja, nenhum, até ontem), eu nunca pensei em esclarecer atrás de mim, como na grande maioria dos casos, os assinantes também vivem para o tempo de vida da aplicação.

Eu brinquei um pouco com o WeakEventHandler de Dustin Campbell , mas ele não funciona com delegates anônimos (não para mim de qualquer maneira).

Existe alguma maneira fora deste problema? Eu realmente gostaria de evitar ter que copiar e colar o código da placa de caldeira por toda a loja.

(Ah, e não se incomode em me perguntar por que estamos criando e destruindo controles o tempo todo, não foi minha decisão de design …)

(PS: É um aplicativo winforms, mas atualizamos para VS2008 e .Net 3.5, devo considerar o uso do padrão Weak Event ?)

(PPS: Boa resposta de Rory , mas se alguém puder inventar um equivalente ao WeakEventHandler que evita que eu tenha que lembrar explicitamente de UnLink / Dispose, isso seria legal …)

EDIT Eu devo admitir que eu trabalhei em torno deste problema “reciclando” os controles em questão. No entanto, a solução voltou para me assombrar como a “chave” que eu estava usando é aparentemente não-única (soluço). Acabei de descobrir outros links aqui (tentei isso – parece ser um pouco fraco demais – GC limpa delegates mesmo que o alvo ainda esteja vivo, mesmo problema com s, oɔɯǝɹ resposta abaixo), aqui (força você a modificar o editor e não realmente trabalhe com delegates anônimos) e aqui (citado como incompleto por Dustin Campbell).

Ocorre-me que o que estou procurando pode ser semanticamente impossível – os closures são projetados para “ficar por perto mesmo depois de eu ter ido embora”.

Eu encontrei outra solução alternativa, então eu vou ficar com isso, enquanto se aguarda uma voz dos deuses .

Eu sei que esta questão é antiga, mas o inferno – eu encontrei, e eu acho que os outros também podem. Estou tentando resolver um problema relacionado e posso ter algumas dicas.

Você mencionou o WeakEventHandler de Dustin Campbell – de fato não pode trabalhar com methods anônimos por design. Eu estava tentando mexer em algo junto que, quando eu percebesse que a) em 99% dos casos eu precisaria de algo assim, sua solução original seria mais segura, eb) naqueles poucos casos em que eu tenho que (nota: tenho para, não “querer porque os lambdas são muito mais bonitos e concisos”) é possível fazê-lo funcionar se você ficar um pouco esperto.

Seu exemplo parece exatamente o tipo de caso único em que ficar um pouco complicado pode resultar em uma solução bastante concisa.

 public static class Linker { public static void Link(Publisher publisher, Control subscriber) { // anonymous method references the subscriber only through weak // references,so its existance doesn't interfere with garbage collection var subscriber_weak_ref = new WeakReference(subscriber); // this instance variable will stay in memory as long as the anonymous // method holds a reference to it we declare and initialize it to // reserve the memory (also, compiler complains about uninitialized // variable otherwise) EventHandler> handler = null; // when the handler is created it will grab references to the local // variables used within, keeping them in memory after the function // scope ends handler = delegate(object sender, ValueEventArgs e) { var subscriber_strong_ref = subscriber_weak_ref.Target as Control; if (subscriber_strong_ref != null) subscriber_strong_ref.Enabled = e.Value; else { // unsubscribing the delegate from within itself is risky, but // because only one instance exists and nobody else has a // reference to it we can do this ((Publisher)sender).EnabledChanged -= handler; // by assigning the original instance variable pointer to null // we make sure that nothing else references the anonymous // method and it can be collected. After this, the weak // reference and the handler pointer itselfwill be eligible for // collection as well. handler = null; } }; publisher.EnabledChanged += handler; } } 

O padrão do WPF Weak Event é oferecido com muita sobrecarga, então nessa situação em particular eu não usaria. Além disso, referenciar a biblioteca principal do WPF em um aplicativo WinForm também parece um pouco pesado.

Se você mantiver uma referência ao representante anônimo e, em seguida, removê-lo quando os controles forem removidos do formulário, isso deverá permitir que os controles e os representantes anônimos sejam coletados como lixo.

Então, algo assim:

 public static class Linker { //(Non-lambda version, I'm not comfortable with lambdas:) public static EventHandler> Link(Publisher publisher, Control subscriber) { EventHandler> handler = delegate(object sender, ValueEventArgs e) { subscriber.Enabled = e.Value; }; publisher.EnabledChanged += handler; return handler; } public static void UnLink(Publisher publisher, EventHandler> handler) { publisher.EnabledChanged -= handler; } } 

Veja o método anônimo do Unsubscribe em C # para um exemplo de remover delegates.

Algum exemplo de código que fiz recentemente, baseado no WeakReference:

 // strongly typed weak reference public class WeakReference : WeakReference where T : class { public WeakReference(T target) : base(target) { } public WeakReference(T target, bool trackResurrection) : base(target, trackResurrection) { } public new T Target { get { return base.Target as T; } set { base.Target = value; } } } // weak referenced generic event handler public class WeakEventHandler : WeakReference> where TEventArgs : EventArgs { public WeakEventHandler(EventHandler target) : base(target) { } protected void Invoke(object sender, TEventArgs e) { if (Target != null) { Target(sender, e); } } public static implicit operator EventHandler(WeakEventHandler weakEventHandler) { if (weakEventHandler != null) { if (weakEventHandler.IsAlive) { return weakEventHandler.Invoke; } } return null; } } // weak reference common event handler public class WeakEventHandler : WeakReference { public WeakEventHandler(EventHandler target) : base(target) { } protected void Invoke(object sender, EventArgs e) { if (Target != null) { Target(sender, e); } } public static implicit operator EventHandler(WeakEventHandler weakEventHandler) { if (weakEventHandler != null) { if (weakEventHandler.IsAlive) { return weakEventHandler.Invoke; } } return null; } } // observable class, fires events public class Observable { public Observable() { Console.WriteLine("new Observable()"); } ~Observable() { Console.WriteLine("~Observable()"); } public event EventHandler OnChange; protected virtual void DoOnChange() { EventHandler handler = OnChange; if (handler != null) { Console.WriteLine("DoOnChange()"); handler(this, EventArgs.Empty); } } public void Change() { DoOnChange(); } } // observer, event listener public class Observer { public Observer() { Console.WriteLine("new Observer()"); } ~Observer() { Console.WriteLine("~Observer()"); } public void OnChange(object sender, EventArgs e) { Console.WriteLine("-> Observer.OnChange({0}, {1})", sender, e); } } // sample usage and test code public static class Program { static void Main() { Observable subject = new Observable(); Observer watcher = new Observer(); Console.WriteLine("subscribe new WeakEventHandler()\n"); subject.OnChange += new WeakEventHandler(watcher.OnChange); subject.Change(); Console.WriteLine("\nObserver = null, GC"); watcher = null; GC.Collect(0, GCCollectionMode.Forced); GC.WaitForPendingFinalizers(); subject.Change(); if (Debugger.IsAttached) { Console.Write("Press any key to continue . . . "); Console.ReadKey(true); } } } 

Gera a seguinte saída:

 new Observable() new Observer() subscribe new WeakEventHandler() DoOnChange() -> Observer.OnChange(ConsoleApplication4.Observable, System.EventArgs) Observer = null, GC ~Observer() DoOnChange() ~Observable() Press any key to continue . . . 

(Observe que a desinscrição (- =) não funciona)

Continuando com a resposta da Egor , eu queria tentar construir uma versão em que eu não precisasse determinar com antecedência qual evento eu gostaria de append.

Eu só consegui fazer isso funcionar com manipuladores de events genéricos: para manipuladores de events ‘padrão’ (ex. FormClosingEventHandler), é um pouco complicado, porque você não pode ter uma restrição de tipo where T : delegate (a menos que seu nome termine com Pônei ).

 private static void SetAnyGenericHandler( Action> add, //to add event listener to publisher Action> remove, //to remove event listener from publisher S subscriber, //ref to subscriber (to pass to consume) Action consume) //called when event is raised* where T : EventArgs where S : class { var subscriber_weak_ref = new WeakReference(subscriber); EventHandler handler = null; handler = delegate(object sender, T e) { var subscriber_strong_ref = subscriber_weak_ref.Target as S; if(subscriber_strong_ref != null) { Console.WriteLine("New event received by subscriber"); consume(subscriber_strong_ref, e); } else { remove(handler); handler = null; } }; add(handler); } 

(* Eu tentei EventHandler consume aqui, mas o código de chamada fica feio porque você tem que converter s para Assinante no lambda de consumo.)

Exemplo de código de chamada, retirado do exemplo acima:

 SetAnyGenericHandler( h => publisher.EnabledChanged += h, h => publisher.EnabledChanged -= h, subscriber, (Subscriber s, ValueEventArgs e) => s.Enabled = e.Value); 

Ou, se você preferir

 SetAnyGenericHandler>( h => publisher.EnabledChanged += h, h => publisher.EnabledChanged -= h, subscriber, (s, e) => s.Enabled = e.Value); 

Seria legal poder passar no Evento como apenas um parâmetro, mas você não pode acessar add / remove de um evento mais do que você pode acessar get / set de uma propriedade (sem fazer reflexões nojentas, eu acho ).