2009-03-23 16 views
24

Wenn geprüft wird, ob ein Event-Handler null ist, wird dies pro Thread ausgeführt?Verwendung des Null-Check-in-Event-Handlers

Gewährleistung jemand hört das Ereignis wie dies geschehen ist:

EventSeven += new DivBySevenHandler(dbsl.ShowOnScreen); 

Wenn ich Code nach diesem Muster füge oben, wo ich für null überprüfen, warum dann würde ich eine NULL-Prüfung (code taken from this site) benötigen. Was vermisse ich?

Auch, was ist die Regel mit Ereignissen und GC?

+3

See: http://www.dailycoding.com/Posts/avoiding_event__null_check.aspx für eine erweiterte Erklärung. –

Antwort

44

Es ist wirklich nicht klar, was du meinst ich fürchte, aber wenn es die Möglichkeit gibt, dass der Delegat null ist, müssen Sie das separat auf jedem Thread überprüfen. Normalerweise würden Sie tun:

public void OnSeven() 
{ 
    DivBySevenHandler handler = EventSeven; 
    if (handler != null) 
    { 
     handler(...); 
    } 
} 

Dies, dass Sie nicht ein NullReferenceException bekommen im Laufe des OnSeven() auch wenn EventSeven Änderungen gewährleistet.

Aber Sie haben Recht, dass Sie den Null-Check nicht benötigen, wenn Sie definitiv einen abonnierten Handler haben. Dies kann leicht in C# 2 mit einem „no-op“ Handler erfolgen:

public event DivBySevenHandler EventSeven = delegate {}; 

Auf der anderen Seite, Sie Macht nur eine Art von Sperren will sicherstellen, dass Sie die „latest haben "Satz von Handlern, wenn Sie Abonnements von verschiedenen Threads erhalten könnten. Ich habe eine example in my threading tutorial, die helfen kann - obwohl ich normalerweise empfehlen würde zu vermeiden, es zu erfordern.

In Bezug auf die Garbage Collection, das Ereignis Verlag endet mit einem Verweis auf den Fall Teilnehmer (d.h. das Ziel des handler) auf. Dies ist nur ein Problem, wenn der Publisher länger als der Abonnent leben soll.

+0

(für das OP/list) re der letzte Punkt, das ist ** besonders ** true von statischen Ereignissen (da das statische Feld nie für die Sammlung in Frage kommt). Statische Ereignisse werden aus diesem Grund am besten vermieden. –

+0

Ich frage mich, ob es ein Problem mit einer EventDelegate-Kategorie von Delegaten gegeben hätte, die dieselbe Semantik wie ein void-Delegat hätte/except /, dass der Aufruf eines null EventDelegate explizit als nop definiert wäre. – supercat

+1

Was ist mit 'EventSeven? .Invoke (....)'. ist es threadsicher? –

2

Es ist immer ratsam, einen Event-Handler zu überprüfen, bevor er ausgelöst wird. Das tue ich auch, wenn ich mich selbst "garantiere", dass es immer gesetzt ist. Wenn ich das später ändere, muss ich nicht alle meine Ereignisfeuern überprüfen. Also für jedes Ereignis habe ich immer eine begleitende OnXXX Methode wie folgt:

private void OnEventSeven() 
{ 
    var handler = EventSeven; 
    if (handler != null) 
    { 
     handler(this, EventArgs.Empty); 
    } 
} 

Dies ist besonders wichtig, wenn der Event-Handler Ihrer Klasse Öffentlichkeit ist seit externe Anrufer hinzufügen und Event-Handler nach Belieben entfernen.

+2

Das ist nicht Thread-sicher. Wenn der letzte Handler sich abmeldet, nachdem das "if" ausgewertet wurde, können Sie eine NullReferenceException beenden. –

+0

Mann, wie kommt es, dass so viele von uns die Thread-UN-Sicherheit darin nicht gesehen haben. Danke für den Weckruf! –

+0

Scheint wie das gleiche (Anti) Muster wird von vielen von uns verwendet. Aber in diesem Fall tut Peter so wie Marc (den Verweis einfangend, aber einen impliziten Typ verwendend). Was ist der Unterschied zwischen dem, was Marc geschrieben hat und Peter? – dotnetdev

0

Wenn Sie meinen, dieses:

public static void OnEventSeven(DivBySevenEventArgs e) 
    { 
     if(EventSeven!=null) 
      EventSeven(new object(),e); 
    }  

Stück Code, dann lautet die Antwort:

Wenn niemand auf die "EventSeven" Event-Handler abonniert dann werden Sie auf eine Null-Referenz-Exception "EventSeven (neues Objekt(), e);"

Und die Regel:

Der Teilnehmer für das Hinzufügen eines Handler verantwortlich ist (+ =) und das Entfernen es (- =), wenn er die Ereignisse nicht mehr erhalten will nicht. Die Garbage Collection wird nach den Standardregeln ausgeführt. Wenn ein Objekt nicht mehr referenziert wird, kann es bereinigt werden.

+3

Das ist nicht threadsicher. Wenn der letzte Handler sich abmeldet, nachdem das "if" ausgewertet wurde, könnte dies zu einer NullreferenceException führen. –

+0

True ... dieses Beispiel ist überall (und ich benutze es auch), stand nie still bei der Thread-Sicherheit dieser! – thijs

+2

Nebenbei bemerkt, ist es nicht Konvention für den Absender, das Objekt zu sein, das das Ereignis auslöst (d. H. 'This'), anstatt ein zufällig neu erzeugtes Objekt? –

47

Das Problem ist, dass, wenn niemand das Ereignis abonniert, es null ist. Und Sie können nicht gegen eine Null aufrufen. Drei Ansätze zur Geist springen:

  • Check für null (siehe unten)
  • fügen Sie ein "nichts tun" Handler: public event EventHandler MyEvent = delegate {};
  • eine Erweiterungsmethode verwenden (siehe unten)

Bei der Überprüfung Für Null, thread-safe, müssen Sie in der Theorie erfassen Sie die Delegiertenreferenz zuerst (falls es zwischen der Prüfung und dem Aufruf ändert):

protected virtual void OnMyEvent() { 
    EventHandler handler = MyEvent; 
    if(handler != null) handler(this, EventArgs.Empty); 
} 

Erweiterungsmethoden haben die ungewöhnliche Eigenschaft, dass sie auf null Instanzen aufrufbar sind ...

public static void SafeInvoke(this EventHandler handler, object sender) 
    { 
     if (handler != null) handler(sender, EventArgs.Empty); 
    } 
    public static void SafeInvoke<T>(this EventHandler<T> handler, 
     object sender, T args) where T : EventArgs 
    { 
     if (handler != null) handler(sender, args); 
    } 

dann können Sie anrufen:

MyEvent.SafeInvoke(this); 

und es ist sowohl null-safe (über den Scheck) und threadsicher (durch einmaliges Lesen der Referenz).

+2

wie die Thread-Sicherheit über die implict Kopie auf dem Stapel – ShuggyCoUk

+0

Schöne Erklärung. Wenn Sie "durch das Lesen .... nur" sagen, ist das threadsicher, da nach dem Lesen das Ziel (Objekt, das gelesen wird - Referenz) sich frei ändern kann. Aber wie liest du die Referenz einmal? Vielleicht habe ich einfach etwas nicht verstanden ... – dotnetdev

+1

@dotnetdev - weil wir es als Methodenargument übergeben; Es liest den aktuellen Wert * auf den Stack *. In SafeInvoke sieht es nur die Kopie auf dem Stapel (das Original kann 200 mal aktualisiert werden, wir werden es nie sehen). Es hilft hier, dass Delegaten unveränderlich sind, also Änderungen wie ... –

0

Mit PostSharp ist es möglich, die kompilierte Assembly in einem Post-Kompilierungsschritt anzupassen. Auf diese Weise können Sie "Aspekte" auf den Code anwenden, um bereichsübergreifende Probleme zu lösen.

Obwohl die Nullprüfungen oder die leere Delegateninitialisierung möglicherweise ein sehr geringes Problem darstellen, habe ich einen Aspekt geschrieben, der sie auflöst, indem ich allen Ereignissen in einer Assembly einen leeren Delegaten hinzufüge.

Es ist Nutzung ist ganz einfach:

[assembly: InitializeEventHandlers(AttributeTargetTypes = "Main.*")] 
namespace Main 
{ 
    ... 
} 

I discussed the aspect in detail on my blog. Falls Sie haben Postsharp, hier ist der Aspekt: ​​

/// <summary> 
/// Aspect which when applied on an assembly or class, initializes all the event handlers (<see cref="MulticastDelegate" />) members 
/// in the class(es) with empty delegates to prevent <see cref="NullReferenceException" />'s. 
/// </summary> 
/// <author>Steven Jeuris</author> 
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Event)] 
[MulticastAttributeUsage(MulticastTargets.Event, AllowMultiple = false)] 
[AspectTypeDependency(AspectDependencyAction.Commute, typeof(InitializeEventHandlersAttribute))] 
[Serializable] 
public class InitializeEventHandlersAttribute : EventLevelAspect 
{ 
    [NonSerialized] 
    Action<object> _addEmptyEventHandler; 


    [OnMethodEntryAdvice, MethodPointcut("SelectConstructors")] 
    public void OnConstructorEntry(MethodExecutionArgs args) 
    { 
     _addEmptyEventHandler(args.Instance); 
    } 

    // ReSharper disable UnusedMember.Local 
    IEnumerable<ConstructorInfo> SelectConstructors(EventInfo target) 
    { 
     return target.DeclaringType.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); 
    } 
    // ReSharper restore UnusedMember.Local 

    public override void RuntimeInitialize(EventInfo eventInfo) 
    { 
     base.RuntimeInitialize(eventInfo); 

     // Construct a suitable empty event handler. 
     MethodInfo delegateInfo = DelegateHelper.MethodInfoFromDelegateType(eventInfo.EventHandlerType); 
     ParameterExpression[] parameters = delegateInfo.GetParameters().Select(p => Expression.Parameter(p.ParameterType)).ToArray(); 
     Delegate emptyDelegate 
      = Expression.Lambda(eventInfo.EventHandlerType, Expression.Empty(), "EmptyDelegate", true, parameters).Compile(); 

     // Create a delegate which adds the empty handler to an instance. 
     _addEmptyEventHandler = instance => eventInfo.AddEventHandler(instance, emptyDelegate); 
    } 
} 

... und die Hilfsmethode nutzt:

/// <summary> 
/// The name of the Invoke method of a Delegate. 
/// </summary> 
const string InvokeMethod = "Invoke"; 


/// <summary> 
/// Get method info for a specified delegate type. 
/// </summary> 
/// <param name = "delegateType">The delegate type to get info for.</param> 
/// <returns>The method info for the given delegate type.</returns> 
public static MethodInfo MethodInfoFromDelegateType(Type delegateType) 
{ 
    Contract.Requires(delegateType.IsSubclassOf(typeof(MulticastDelegate)), "Given type should be a delegate."); 

    return delegateType.GetMethod(InvokeMethod); 
} 
18

Ich grabe eine sehr alte Post - aber ich will nur fügen Sie einige kurze Informationen über die C# 6 hinzu.0-Syntax:

Es ist nun möglich, diese zu ersetzen:

var handler = EventSeven; 

if (handler != null) 
    handler.Invoke(this, EventArgs.Empty); 

mit diesem:

handler?.Invoke(this, EventArgs.Empty); 


EDIT: In Kombination mit Ausdruck-bodied Mitglieder, Sie kann folgenden Code kurzschließen:

protected virtual void OnMyEvent() 
{ 
    EventHandler handler = MyEvent; 
    handler?.Invoke(this, EventArgs.Empty); 
} 

bis auf einen Einzeiler:

protected virtual void OnMyEvent() => MyEvent?.Invoke(this, EventArgs.Empty); 


See MSDN for more information about the null-conditional operator
Siehe this blog über Expression-bodied Mitglieder

+0

Ich versuche zu verstehen, ob das 'MyEvent? .Invoke' so sicher wie' handler? .Invoke' ist. Ist die Null-Ausbreitungsprüfung atomar? – Teejay

+1

Eigentlich scheint es ja. https://codeblog.jonskeet.uk/2015/01/30/clean-event-handlers-invocation-with-c-6/ – Teejay

Verwandte Themen