2008-11-15 13 views
59

W przeglądzie kodu, natknąłem się na tym (uproszczony) fragment kodu wyrejestrowania obsługi zdarzeń:Jak poprawnie wyrejestrować obsługi zdarzeń

Fire -= new MyDelegate(OnFire); 

myślałem, że to nie wyrejestrować obsługi zdarzeń, ponieważ tworzy nowego delegata, który nigdy wcześniej nie był zarejestrowany. Ale szukając MSDN znalazłem kilka próbek kodu, które używają tego idiomu.

Więc zacząłem eksperyment:

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

} 

Ku mojemu zaskoczeniu, dodaje się:

  1. Fire("Hello 1"); produkowane dwie wiadomości, jak oczekiwano.
  2. Fire("Hello 2"); wyprodukował jedną wiadomość!
    To przekonało mnie, że wyrejestrowanie delegatów new działa!
  3. Fire("Hello 3"); rzucił NullReferenceException.
    Debugowanie kodu pokazało, że Fire jest null po wyrejestrowaniu zdarzenia.

Wiem, że dla programów obsługi zdarzeń i delegatów kompilator generuje dużo kodu za sceną. Ale nadal nie rozumiem, dlaczego moje rozumowanie jest złe.

Czego mi brakuje?

dodatkowe pytanie: z faktu, że Fire jest null gdy są zarejestrowane żadne zdarzenia, dochodzę do wniosku, że wszędzie zdarzenie jest zwolniony, konieczne jest sprawdzenie przed null.

Odpowiedz

78

Domyślna implementacja kompilator C# za dodawanie obsługi zdarzeń wywołuje Delegate.Combine, podczas usuwania programu obsługi zdarzeń wywołuje Delegate.Remove:

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

wdrażanie Ramowej z dnia Delegate.Remove nie patrzy na samego obiektu MyDelegate, ale u Metoda, którą delegat określa (Program.OnFire). W związku z tym podczas tworzenia subskrypcji istniejącej procedury obsługi zdarzenia jest całkowicie bezpieczne utworzenie nowego obiektu MyDelegate. Z powodu tego, # kompilator C pozwala na stosowanie składni skróconej (który generuje dokładnie ten sam kod za kulisami) podczas dodawania/usuwania programów obsługi zdarzeń: można pominąć new MyDelegate udział:

Fire += OnFire; 
Fire -= OnFire; 

Kiedy ostatni delegata jest usuwany z obsługi zdarzeń, Delegate.Remove zwraca wartość null. Jak już się dowiedziałem, jest to niezbędne w celu sprawdzenia zdarzenia przed zerową przed podniesieniem go:

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

Jest przypisany do tymczasowej zmiennej lokalnej do obrony przed ewentualnym wyścigu z wypisywania obsługi zdarzeń na innych wątkach. (Zobacz my blog post, aby uzyskać szczegółowe informacje na temat bezpieczeństwa wątków przypisywania funkcji obsługi zdarzeń do zmiennej lokalnej.) Innym sposobem obrony przed tym problemem jest utworzenie pustego delegata, który jest zawsze subskrybowany; podczas gdy ten korzysta z trochę więcej pamięci, obsługi zdarzeń nie może być null (i kod może być prostsze):

public static event MyDelegate Fire = delegate { }; 
+3

Tak. Uzupełnienie: to mylić mnie przez jakiś czas też, zwłaszcza, że ​​w C# można zrobić Ogień + = new MyDelegate (OnFire) lub Ogień + = OnFire; Ten ostatni wydaje się być prostszy, ale jest po prostu cukrem syntaktycznym dla pierwszego. –

+0

@Nicholas Piasecki: Dzięki; Zaktualizowałem swoją odpowiedź, aby zauważyć, że (raczej użyteczny) skrót. –

+0

Warto zauważyć, że dodanie delegata multiemisji i późniejsze anulowanie subskrypcji może przynieść niepoprawne wyniki, jeśli dowolna z metod w ramach tego delegata multiemisji jest również subskrybowana i subskrybowana indywidualnie. – supercat

15

Należy zawsze sprawdzić, czy delegat ma cele (jego wartość jest null) przed odpaleniem go . Jak już wspomniano, jednym ze sposobów na to jest zasubskrybowanie anonimowej metody, która nie zostanie usunięta.

public event MyDelegate Fire = delegate {}; 

Jednak jest to po prostu hack, aby uniknąć NullReferenceExceptions.

Po prostu sprawdzanie, czy delegat ma wartość zerową przed wywołaniem, nie jest zabezpieczone przed wątkami, ponieważ inny wątek może wyrejestrować się po sprawdzeniu zerowym i uczynić go pustym podczas wywoływania. Jest inne rozwiązanie jest skopiowanie delegata do zmiennej tymczasowej:

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

Niestety, kompilator JIT może zoptymalizować kod, wyeliminować zmiennej tymczasowej i używać oryginalnego delegata. (Zgodnie Juval Lowy - Programowanie .NET Components)

Aby uniknąć tego problemu, można użyć metody, która akceptuje delegata jako parametr:

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

Należy pamiętać, że bez MethodImpl (NoInlining) przypisują JIT kompilator może wbudować metodę, dzięki czemu jest bezwartościowy. Ponieważ delegaci są niezmienni, ta implementacja jest bezpieczna dla wątków. Możesz użyć tej metody jako:

FireEvent(Fire,"Hello 3"); 
+0

Bardzo dziękuję za wyjaśnienie kwestii związanych z JIT. Pułapki i pułapki na całym świecie. – gyrolf

+7

Właściwie nie jest to możliwe z CLR 2.0 Microsoftu, ze względu na jego silniejszy model pamięci: http://code.logos.com/blog/2008/11/events_and_threads_part_4.html –

Powiązane problemy