70 Stimmen

So heben Sie die Registrierung eines Ereignis-Handlers korrekt auf

Bei einer Codeüberprüfung bin ich über dieses (vereinfachte) Codefragment zum Aufheben der Registrierung eines Ereignishandlers gestolpert:

 Fire -= new MyDelegate(OnFire);

Ich dachte, dass dies die Registrierung des Ereignis-Handlers nicht aufhebt, da ein neuer Delegat erstellt wird, der noch nie registriert wurde. Aber beim Durchsuchen von MSDN fand ich mehrere Codebeispiele, die dieses Idiom verwenden.

Also habe ich ein Experiment gestartet:

internal class Program
{
    public delegate void MyDelegate(string msg);
    public static event MyDelegate Fire;

    private static void Main(string[] args)
    {
        Fire += new MyDelegate(OnFire);
        Fire += new MyDelegate(OnFire);
        Fire("Hello 1");
        Fire -= new MyDelegate(OnFire);
        Fire("Hello 2");
        Fire -= new MyDelegate(OnFire);
        Fire("Hello 3");
    }

    private static void OnFire(string msg)
    {
        Console.WriteLine("OnFire: {0}", msg);
    }

}

Zu meiner Überraschung geschah dann Folgendes:

  1. Fire("Hello 1"); erzeugte wie erwartet zwei Meldungen.
  2. Fire("Hello 2"); eine Nachricht produziert!
    Dies hat mich davon überzeugt, dass die Abmeldung new Delegierte arbeiten!
  3. Fire("Hello 3"); warf eine NullReferenceException .
    Das Debuggen des Codes ergab, dass Fire es null nachdem Sie das Ereignis abgemeldet haben.

Ich weiß, dass der Compiler für Event-Handler und Delegate eine Menge Code hinter der Szene generiert. Aber ich verstehe immer noch nicht, warum meine Argumentation falsch ist.

Was übersehe ich?

Zusätzliche Frage: Aus der Tatsache, dass Fire es null wenn keine Ereignisse registriert werden, schließe ich daraus, dass überall dort, wo ein Ereignis ausgelöst wird, eine Prüfung gegen null erforderlich ist.

89voto

Bradley Grainger Punkte 25874

Die Standardimplementierung des C#-Compilers für das Hinzufügen eines Ereignis-Handlers ruft Delegate.Combine , während das Entfernen eines Ereignishandlers Delegate.Remove :

Fire = (MyDelegate) Delegate.Remove(Fire, new MyDelegate(Program.OnFire));

Die Umsetzung des Rahmens von Delegate.Remove schaut nicht auf die MyDelegate Objekt selbst, sondern bei der Methode, auf die sich der Delegat bezieht ( Program.OnFire ). Es ist also vollkommen sicher, eine neue MyDelegate Objekt, wenn ein vorhandener Event-Handler abgemeldet wird. Aus diesem Grund erlaubt der C#-Compiler die Verwendung einer Kurzsyntax (die hinter den Kulissen genau denselben Code erzeugt) beim Hinzufügen/Entfernen von Ereignishandlern: Sie können die new MyDelegate Teil:

Fire += OnFire;
Fire -= OnFire;

Wenn der letzte Delegierte aus dem Ereignis-Handler entfernt wird, Delegate.Remove gibt null zurück. Wie Sie herausgefunden haben, ist es wichtig, das Ereignis gegen null zu prüfen, bevor es ausgelöst wird:

MyDelegate handler = Fire;
if (handler != null)
    handler("Hello 3");

Sie wird einer temporären lokalen Variablen zugewiesen, um eine mögliche Wettlaufsituation mit abmeldenden Ereignishandlern in anderen Threads zu verhindern. (Siehe mein Blogbeitrag für Details über die Thread-Sicherheit der Zuweisung des Ereignis-Handlers an eine lokale Variable). Eine andere Möglichkeit, sich gegen dieses Problem zu wehren, besteht darin, einen leeren Delegaten zu erstellen, der immer abonniert wird; dies verbraucht zwar etwas mehr Speicher, aber der Ereignis-Handler kann nie null sein (und der Code kann einfacher sein):

public static event MyDelegate Fire = delegate { };

15voto

user37325 Punkte 169

Sie sollten immer prüfen, ob ein Delegat keine Ziele hat (sein Wert ist null), bevor Sie ihn auslösen. Wie bereits erwähnt, besteht eine Möglichkeit darin, sich mit einer anonymen Methode anzumelden, die nicht entfernt werden kann.

public event MyDelegate Fire = delegate {};

Dies ist jedoch nur ein Trick, um NullReferenceExceptions zu vermeiden.

Einfach nur prüfen, ob ein Delegat vor dem Aufruf null ist, ist nicht thread-sicher, da ein anderer Thread nach der Null-Prüfung deregistrieren kann und es beim Aufruf null macht. Es gibt eine andere Lösung ist, den Delegaten in eine temporäre Variable zu kopieren:

public event MyDelegate Fire;
public void FireEvent(string msg)
{
    MyDelegate temp = Fire;
    if (temp != null)
        temp(msg);
}

Leider kann der JIT-Compiler den Code optimieren, die temporäre Variable entfernen und den ursprünglichen Delegaten verwenden. (nach Juval Lowy - Programmierung von .NET-Komponenten)

Um dieses Problem zu vermeiden, können Sie eine Methode verwenden, die einen Delegaten als Parameter akzeptiert:

[MethodImpl(MethodImplOptions.NoInlining)]
public void FireEvent(MyDelegate fire, string msg)
{
    if (fire != null)
        fire(msg);
}

Beachten Sie, dass ohne das Attribut MethodImpl(NoInlining) der JIT-Compiler die Methode inline schalten könnte, was sie wertlos macht. Da Delegierte unveränderlich sind, ist diese Implementierung thread-sicher. Sie könnten diese Methode als verwenden:

FireEvent(Fire,"Hello 3");

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X