Ist es notwendig, Ereignishandler in C # explizit zu entfernen?

120

Ich habe eine Klasse, die einige Veranstaltungen anbietet. Diese Klasse wird global deklariert, aber nicht auf dieser globalen Deklaration instanziiert - sie wird nach Bedarf in den Methoden instanziiert, die sie benötigen.

Jedes Mal, wenn diese Klasse in einer Methode benötigt wird, wird sie instanziiert und Ereignishandler werden registriert. Müssen die Ereignishandler explizit entfernt werden, bevor die Methode den Gültigkeitsbereich verlässt?

Wenn die Methode den Gültigkeitsbereich verlässt, geht auch die Instanz der Klasse verloren. Hat das Verlassen von Ereignishandlern, die bei dieser Instanz registriert sind, deren Gültigkeitsbereich überschritten wird, Auswirkungen auf den Speicherbedarf? (Ich frage mich, ob der Ereignishandler den GC davon abhält, die Klasseninstanz als nicht mehr referenziert anzusehen.)

rp.
quelle

Antworten:

184

In Ihrem Fall ist alles in Ordnung. Es ist das Objekt, das die Ereignisse veröffentlicht, das die Ziele der Ereignishandler am Leben erhält . Also wenn ich habe:

publisher.SomeEvent += target.DoSomething;

hat dann publishereinen Bezug, targetaber nicht umgekehrt.

In Ihrem Fall ist der Herausgeber für die Speicherbereinigung berechtigt (vorausgesetzt, es gibt keine anderen Verweise darauf), sodass die Tatsache, dass er einen Verweis auf die Event-Handler-Ziele enthält, irrelevant ist.

Der schwierige Fall ist, wenn der Publisher langlebig ist, die Abonnenten dies jedoch nicht möchten. In diesem Fall müssen Sie die Handler abbestellen. Angenommen, Sie haben einen Datenübertragungsdienst, mit dem Sie asynchrone Benachrichtigungen über Bandbreitenänderungen abonnieren können, und das Übertragungsdienstobjekt ist langlebig. Wenn wir das tun:

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;

(Sie möchten tatsächlich einen finally-Block verwenden, um sicherzustellen, dass der Ereignishandler nicht durchgesickert ist.) Wenn wir uns nicht abmelden, BandwidthUIwürde der mindestens so lange wie der Übertragungsdienst funktionieren .

Persönlich stoße ich selten darauf - normalerweise, wenn ich ein Ereignis abonniere, lebt das Ziel dieses Ereignisses mindestens so lange wie der Herausgeber - ein Formular hält so lange wie die Schaltfläche, die sich darauf befindet. Es lohnt sich, über dieses potenzielle Problem Bescheid zu wissen, aber ich denke, einige Leute machen sich darüber Sorgen, wenn sie es nicht brauchen, weil sie nicht wissen, in welche Richtung die Referenzen gehen.

EDIT: Dies ist, um Jonathan Dickinsons Kommentar zu beantworten. Schauen Sie sich zunächst die Dokumente für Delegate.Equals (Objekt) an, in denen das Gleichheitsverhalten eindeutig angegeben ist.

Zweitens ist hier ein kurzes, aber vollständiges Programm, das zeigt, wie das Abbestellen funktioniert:

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();
    }
}

Ergebnisse:

Raising Foo
Subscriber.FooHandler()
Raising Foo
No handlers

(Getestet auf Mono und .NET 3.5SP1.)

Weiter bearbeiten:

Dies soll beweisen, dass ein Event-Publisher gesammelt werden kann, solange noch Verweise auf einen Abonnenten vorhanden sind.

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);
    }
}

Ergebnisse (in .NET 3.5SP1; Mono scheint sich hier etwas seltsam zu verhalten. Wird dies einige Zeit untersuchen):

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
quelle
2
Ich stimme dem zu, aber wenn möglich, können Sie kurz auf ein Beispiel eingehen oder sich vorzugsweise darauf beziehen, was Sie unter "aber die Abonnenten wollen nicht sein" verstehen?
Peter McG
@ Jon: Sehr geschätzt, es ist nicht üblich, aber wie Sie sagen, ich habe Leute gesehen, die sich darüber unnötig Sorgen gemacht haben.
Peter McG
- = Funktioniert nicht. - = Führt zu einem neuen Delegaten, und Delegierte überprüfen die Gleichheit nicht mithilfe der Zielmethode, sondern führen ein Objekt aus.ReferenceEquals () für den Delegaten. Der neue Delegat ist in der Liste nicht vorhanden: Er hat keine Auswirkung (und wirft seltsamerweise keinen Fehler aus).
Jonathan C Dickinson
2
@ Jonathan: Nein, Delegierte überprüfen die Gleichheit mit der Zielmethode. Wird sich in einer Bearbeitung beweisen.
Jon Skeet
Ich gebe zu. Ich wurde mit anonymen Delegierten verwechselt.
Jonathan C Dickinson
8

In deinem Fall geht es dir gut. Ich habe Ihre Frage ursprünglich rückwärts gelesen, dass ein Abonnent nicht mehr in den Geltungsbereich fällt, nicht der Herausgeber . Wenn der Ereignisverleger den Geltungsbereich verlässt, gehen die Verweise auf den Abonnenten (natürlich nicht auf den Abonnenten selbst!) Mit und es besteht keine Notwendigkeit, sie explizit zu entfernen.

Meine ursprüngliche Antwort ist unten, über das, was passiert , wenn Sie ein Ereignis erstellen Teilnehmer und lassen Sie es von Umfang hinausgehen , ohne abzumelden. Es trifft nicht auf Ihre Frage zu, aber ich werde es für die Geschichte an Ort und Stelle lassen.

Wenn die Klasse weiterhin über Ereignishandler registriert ist, ist sie weiterhin erreichbar. Es ist immer noch ein lebendes Objekt. Ein GC, der einem Ereignisdiagramm folgt, findet ihn verbunden. Ja, Sie möchten die Ereignishandler explizit entfernen.

Nur weil das Objekt außerhalb des Bereichs seiner ursprünglichen Zuordnung liegt, bedeutet dies nicht, dass es ein Kandidat für GC ist. Solange eine Live-Referenz erhalten bleibt, ist sie live.

Eddie
quelle
1
Ich glaube nicht , jeder Abmelde notwendig ist hier - der GC sieht Referenzen aus dem Ereignis Verlag, es nicht zu, und es ist der Herausgeber wir hier betroffen sind.
Jon Skeet
@ Jon Skeet: Du hast recht. Ich habe die Frage rückwärts gelesen. Ich habe meine Antwort korrigiert, um die Realität widerzuspiegeln.
Eddie