É necessário remover explicitamente os manipuladores de eventos em C #

120

Eu tenho uma aula que oferece alguns eventos. Essa classe é declarada globalmente, mas não é instanciada nessa declaração global - é instanciada conforme a necessidade nos métodos que precisam dela.

Cada vez que a classe é necessária em um método, ela é instanciada e os manipuladores de eventos são registrados. É necessário remover os manipuladores de eventos explicitamente antes que o método fique fora do escopo?

Quando o método sai do escopo, o mesmo ocorre com a instância da classe. A saída de manipuladores de eventos registrados com a instância que está saindo do escopo tem uma implicação de pegada de memória? (Gostaria de saber se o manipulador de eventos impede o GC de ver a instância da classe como não sendo mais referenciada.)

rp.
fonte

Respostas:

184

No seu caso, está tudo bem. É o objeto que publica os eventos que mantém os alvos dos manipuladores de eventos ativos. Então, se eu tiver:

publisher.SomeEvent += target.DoSomething;

então publishertem uma referência, targetmas não o contrário.

No seu caso, o editor será elegível para a coleta de lixo (supondo que não haja outras referências a ele), portanto, o fato de ter uma referência aos destinos do manipulador de eventos é irrelevante.

O caso complicado é quando o editor tem vida longa, mas os assinantes não querem, nesse caso, é necessário cancelar a inscrição dos manipuladores. Por exemplo, suponha que você tenha algum serviço de transferência de dados que permita assinar notificações assíncronas sobre alterações na largura de banda e que o objeto do serviço de transferência tenha vida longa. Se fizermos isso:

BandwidthUI ui = new BandwidthUI();
transferService.BandwidthChanged += ui.HandleBandwidthChange;
// Suppose this blocks until the transfer is complete
transferService.Transfer(source, destination);
// We now have to unsusbcribe from the event
transferService.BandwidthChanged -= ui.HandleBandwidthChange;

(Na verdade, você deseja usar um bloco finalmente para garantir que você não vaze o manipulador de eventos.) Se não cancelássemos a inscrição, eles BandwidthUIpermaneceriam pelo menos enquanto o serviço de transferência.

Pessoalmente, raramente me deparo com isso - geralmente, se eu assino um evento, o destino desse evento dura pelo menos o tempo que o editor - um formulário dura o mesmo que o botão que está nele, por exemplo. Vale a pena conhecer esse possível problema, mas acho que algumas pessoas se preocupam com isso quando não precisam, porque não sabem para que lado as referências vão.

EDIT: Isso é para responder ao comentário de Jonathan Dickinson. Primeiro, observe os documentos para Delegate.Equals (object) que claramente fornecem o comportamento de igualdade.

Em segundo lugar, aqui está um programa curto, mas completo, para mostrar o funcionamento da desinscrição:

using System;

public class Publisher
{
    public event EventHandler Foo;

    public void RaiseFoo()
    {
        Console.WriteLine("Raising Foo");
        EventHandler handler = Foo;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
        else
        {
            Console.WriteLine("No handlers");
        }
    }
}

public class Subscriber
{
    public void FooHandler(object sender, EventArgs e)
    {
        Console.WriteLine("Subscriber.FooHandler()");
    }
}

public class Test
{
    static void Main()
    {
         Publisher publisher = new Publisher();
         Subscriber subscriber = new Subscriber();
         publisher.Foo += subscriber.FooHandler;
         publisher.RaiseFoo();
         publisher.Foo -= subscriber.FooHandler;
         publisher.RaiseFoo();
    }
}

Resultados:

Raising Foo
Subscriber.FooHandler()
Raising Foo
No handlers

(Testado em Mono e .NET 3.5SP1.)

Edição adicional:

Isso serve para provar que um editor de eventos pode ser coletado enquanto ainda há referências a um assinante.

using System;

public class Publisher
{
    ~Publisher()
    {
        Console.WriteLine("~Publisher");
        Console.WriteLine("Foo==null ? {0}", Foo == null);
    }

    public event EventHandler Foo;
}

public class Subscriber
{
    ~Subscriber()
    {
        Console.WriteLine("~Subscriber");
    }

    public void FooHandler(object sender, EventArgs e) {}
}

public class Test
{
    static void Main()
    {
         Publisher publisher = new Publisher();
         Subscriber subscriber = new Subscriber();
         publisher.Foo += subscriber.FooHandler;

         Console.WriteLine("No more refs to publisher, "
             + "but subscriber is alive");
         GC.Collect();
         GC.WaitForPendingFinalizers();         

         Console.WriteLine("End of Main method. Subscriber is about to "
             + "become eligible for collection");
         GC.KeepAlive(subscriber);
    }
}

Resultados (no .NET 3.5SP1; o Mono parece se comportar de maneira um pouco estranha aqui. Iremos analisar isso em algum momento):

No more refs to publisher, but subscriber is alive
~Publisher
Foo==null ? False
End of Main method. Subscriber is about to become eligible for collection
~Subscriber
Jon Skeet
fonte
2
Eu concordo com isso, mas, se possível, você pode elaborar brevemente ou preferencialmente se referir a um exemplo do que você quer dizer com "mas os assinantes não querem ser"?
6119 Peter McG
@ Jon: Muito apreciado, não é comum, mas como você diz, tenho visto pessoas se preocuparem com isso desnecessariamente.
6269 Peter McG
- = Não funciona. - = Irá resultar em um novo delegado, e os delegados não verificam a igualdade usando o método de destino, eles fazem um objeto.ReferenceEquals () no delegado. O novo delegado não existe na lista: não tem efeito (e não gera um erro suficientemente estranho).
Jonathan C Dickinson
2
@ Jonathan: Não, os delegados verificam a igualdade usando o método de destino. Provará em uma edição.
911 Jon Skeet
Eu concordo. Eu fiquei confuso com delegados anônimos.
Jonathan C Dickinson
8

No seu caso, você está bem. Originalmente, li sua pergunta de trás para frente, dizendo que um assinante estava fora do escopo, não o editor . Se o publicador do evento ficar fora do escopo, as referências ao assinante (e não ao próprio assinante, é claro!) Vão com ele e não há necessidade de removê-lo explicitamente.

Minha resposta original está abaixo, sobre o que acontece se você criar um assinante de evento e deixá-lo sair do escopo sem cancelar a inscrição. Isso não se aplica à sua pergunta, mas eu a deixarei no lugar da história.

Se a classe ainda estiver registrada por meio de manipuladores de eventos, ainda poderá ser alcançada. Ainda é um objeto ativo. Um GC seguindo um gráfico de eventos o encontrará conectado. Sim, você desejará remover explicitamente os manipuladores de eventos.

Só porque o objeto está fora do escopo de sua alocação original não significa que ele é um candidato ao GC. Enquanto uma referência ao vivo permanecer, ela estará ao vivo.

Eddie
fonte
1
Não acredito que seja necessário cancelar a inscrição aqui - o GC vê referências do editor do evento, não dele, e é com o editor que estamos preocupados aqui.
Jon Skeet
@ Jon Skeet: Você está certo. Eu li a pergunta ao contrário. Corrigi minha resposta para refletir a realidade.
Eddie