it-swarm.com.de

So heben Sie die Registrierung eines Ereignishandlers auf

Bei einer Codeüberprüfung bin ich über dieses (vereinfachte) Codefragment gestolpert, um die Registrierung eines Ereignishandlers aufzuheben:

 Fire -= new MyDelegate(OnFire);

Ich dachte, dass das den Ereignishandler nicht abmeldet, da es einen neuen Delegaten erstellt, der noch nie zuvor registriert worden war. Bei der Suche nach MSDN habe ich jedoch mehrere Codebeispiele gefunden, die diese Redewendung 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 Folgendes:

  1. Fire("Hello 1"); hat wie erwartet zwei Nachrichten erzeugt.
  2. Fire("Hello 2"); hat eine Nachricht erzeugt!
    Das hat mich überzeugt, dass das Abmelden von new Delegierten funktioniert!
  3. Fire("Hello 3"); hat ein NullReferenceException geworfen.
    Das Debuggen des Codes hat gezeigt, dass Firenull ist, nachdem die Registrierung des Ereignisses aufgehoben wurde.

Ich weiß, dass der Compiler für Event-Handler und Delegierte viel Code hinter den Kulissen generiert. Aber ich verstehe immer noch nicht, warum meine Argumentation falsch ist.

Was vermisse ich?

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

64
gyrolf

Die Standardimplementierung des C # -Compilers zum Hinzufügen eines Ereignishandlers ruft Delegate.Combine Auf, während das Entfernen eines Ereignishandlers Delegate.Remove Aufruft:

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

Die Framework-Implementierung von Delegate.Remove Betrachtet nicht das MyDelegate -Objekt selbst, sondern die Methode, auf die sich der Delegat bezieht (Program.OnFire). Auf diese Weise ist es absolut sicher, ein neues MyDelegate -Objekt zu erstellen, wenn Sie eine vorhandene Ereignisbehandlungsroutine abbestellen. Aus diesem Grund können Sie im C # -Compiler beim Hinzufügen/Entfernen von Ereignishandlern eine Kurzsyntax verwenden (die im Hintergrund genau denselben Code generiert): Sie können den Teil new MyDelegate Weglassen:

Fire += OnFire;
Fire -= OnFire;

Wenn der letzte Delegat aus dem Ereignishandler entfernt wird, gibt Delegate.Remove Null zurück. Wie Sie herausgefunden haben, ist es wichtig, das Ereignis vor dem Auslösen auf null zu prüfen:

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

Es ist einer temporären lokalen Variablen zugewiesen, um sich gegen eine mögliche Race-Bedingung zu verteidigen, indem Event-Handler in anderen Threads abgemeldet werden. (Weitere Informationen zur Thread-Sicherheit beim Zuweisen des Ereignishandlers zu einer lokalen Variablen finden Sie unter my blog post .) Sie können sich auch gegen dieses Problem wehren, indem Sie einen leeren Delegaten erstellen, der immer abonniert ist. Während dies etwas mehr Speicher benötigt, kann die Ereignisbehandlungsroutine niemals null sein (und der Code kann einfacher sein):

public static event MyDelegate Fire = delegate { };
82

Sie sollten immer überprü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, eine anonyme Methode zu verwenden, die nicht entfernt wird.

public event MyDelegate Fire = delegate {};

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

Nur einfach zu prüfen, ob ein Delegat vor dem Aufrufen null ist, ist nicht threadsicher, da sich ein anderer Thread nach der Nullprüfung abmelden und beim Aufrufen null machen kann. Eine andere Lösung besteht darin, 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. (gemäß 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 der JIT-Compiler ohne das Attribut MethodImpl (NoInlining) die Methode inline setzen könnte, wodurch sie wertlos wird. Da Delegates unveränderlich sind, ist diese Implementierung threadsicher. Sie können diese Methode verwenden als:

FireEvent(Fire,"Hello 3");
15
user37325