event Action <> vs event EventHandler <>

144

Gibt es einen Unterschied zwischen deklarieren event Action<>und event EventHandler<>.

Angenommen, es spielt keine Rolle, welches Objekt tatsächlich ein Ereignis ausgelöst hat.

beispielsweise:

public event Action<bool, int, Blah> DiagnosticsEvent;

vs.

public event EventHandler<DiagnosticsArgs> DiagnosticsEvent;

class DiagnosticsArgs : EventArgs
{
    public DiagnosticsArgs(bool b, int i, Blah bl)
    {...}
    ...
}

Die Nutzung wäre in beiden Fällen nahezu gleich:

obj.DiagnosticsEvent += HandleDiagnosticsEvent;

Es gibt einige Dinge, die ich an event EventHandler<>Mustern nicht mag :

  • Zusätzliche Typdeklaration von EventArgs abgeleitet
  • Zwangsübergabe der Objektquelle - oft kümmert es niemanden

Mehr Code bedeutet mehr Code, der ohne klaren Vorteil gepflegt werden muss.

Daher bevorzuge ich event Action<>

Nur wenn in Aktion <> zu viele Typargumente vorhanden sind, ist eine zusätzliche Klasse erforderlich.

Boris Lipschitz
quelle
2
plusOne (ich habe gerade das System geschlagen) für "niemand kümmert sich"
Hyankov
@plusOne: Ich muss den Absender wirklich kennen! Sagen Sie, dass etwas passiert und Sie wissen möchten, wer es getan hat. Dort benötigen Sie 'Objektquelle' (auch bekannt als Absender).
Kamran Bigdely
Absender kann eine Eigenschaft in der Nutzlast der Veranstaltung sein
Thanasis Ioannidis

Antworten:

67

Der Hauptunterschied besteht darin, dass bei Verwendung Action<>Ihres Ereignisses das Entwurfsmuster praktisch aller anderen Ereignisse im System nicht eingehalten wird, was ich als Nachteil betrachten würde.

Ein Vorteil des dominierenden Entwurfsmusters (abgesehen von der Kraft der Gleichheit) ist, dass Sie das EventArgsObjekt mit neuen Eigenschaften erweitern können, ohne die Signatur des Ereignisses zu ändern. Dies wäre immer noch möglich, wenn Sie es verwenden würden Action<SomeClassWithProperties>, aber ich sehe keinen Sinn darin, in diesem Fall nicht den regulären Ansatz zu verwenden.

Fredrik Mörk
quelle
Könnte die Verwendung Action<>zu Speicherverlusten führen? Ein Nachteil des EventHandlerEntwurfsmusters sind Speicherlecks. Es sollte auch darauf hingewiesen werden, dass es mehrere Event-Handler geben kann, aber nur eine Aktion
Luke T O'Brien,
4
@ LukeTO'Brien: Ereignisse sind im Wesentlichen Delegierte, daher bestehen die gleichen Möglichkeiten für Speicherverluste Action<T>. Außerdem Action<T> kann auf mehrere Methoden verwiesen werden. Hier ist eine Zusammenfassung, die dies demonstriert: gist.github.com/fmork/4a4ddf687fa8398d19ddb2df96f0b434
Fredrik Mörk
88

Basierend auf einigen der vorherigen Antworten werde ich meine Antwort in drei Bereiche unterteilen.

Erstens physikalische Einschränkungen bei der Verwendung im Action<T1, T2, T2... >Vergleich zur Verwendung einer abgeleiteten Klasse von EventArgs. Es gibt drei Möglichkeiten: Erstens: Wenn Sie die Anzahl oder den Typ der Parameter ändern, muss jede abonnierte Methode geändert werden, um dem neuen Muster zu entsprechen. Wenn dies ein öffentlich zugängliches Ereignis ist, das Assemblys von Drittanbietern verwenden, und die Möglichkeit besteht, dass sich die Ereignisargumente ändern, ist dies ein Grund, aus Gründen der Konsistenz eine benutzerdefinierte Klasse zu verwenden, die von Ereignisargumenten abgeleitet ist (denken Sie daran, dass Sie dies immer noch tun KÖNNEN) use an Action<MyCustomClass>) Zweitens Action<T1, T2, T2... >verhindert using , dass Sie Feedback an die aufrufende Methode zurückgeben, es sei denn, Sie haben ein Objekt (z. B. mit einer Handled-Eigenschaft), das zusammen mit der Aktion übergeben wird. Drittens Sie keine Parameter bekommen genannt, wenn Sie also 3 vorbei sind bool‚s ein int, zweistringund a DateTime, Sie haben keine Ahnung, was die Bedeutung dieser Werte ist. Als Randnotiz können Sie immer noch die Methode "Dieses Ereignis sicher auslösen, während Sie es noch verwenden Action<T1, T2, T2... >" verwenden.

Zweitens Auswirkungen auf die Konsistenz. Wenn Sie ein großes System haben, mit dem Sie bereits arbeiten, ist es fast immer besser, dem Design des restlichen Systems zu folgen, es sei denn, Sie haben einen sehr guten Grund, dies nicht zu tun. Wenn Sie öffentlich mit Ereignissen konfrontiert sind, die beibehalten werden müssen, kann die Möglichkeit, abgeleitete Klassen zu ersetzen, wichtig sein. Vergiss das nicht.

Drittens, in der Praxis, stelle ich persönlich fest, dass ich dazu neige, viele einmalige Ereignisse für Dinge wie Eigenschaftsänderungen zu erstellen, mit denen ich interagieren muss (insbesondere bei MVVM mit Ansichtsmodellen, die miteinander interagieren) oder wo das Ereignis stattgefunden hat ein einzelner Parameter. Meistens nehmen diese Ereignisse die Form von public event Action<[classtype], bool> [PropertyName]Changed;oder an public event Action SomethingHappened;. In diesen Fällen gibt es zwei Vorteile. Zuerst bekomme ich einen Typ für die ausstellende Klasse. Wenn MyClassdeklariert und die einzige Klasse ist, die das Ereignis auslöst, erhalte ich eine explizite Instanz MyClass, mit der ich im Ereignishandler arbeiten kann. Zweitens ist für einfache Ereignisse wie Eigenschaftsänderungsereignisse die Bedeutung der Parameter offensichtlich und wird im Namen des Ereignishandlers angegeben, und ich muss nicht unzählige Klassen für diese Art von Ereignissen erstellen.

Paul Rohde
quelle
Super Blog-Beitrag. Auf jeden Fall eine Lektüre wert, wenn Sie diesen Thread lesen!
Vexir
1
Detaillierte und gut durchdachte Antwort, die die
Gründe
18

Zum größten Teil würde ich sagen, folgen Sie dem Muster. Ich bin davon abgewichen, aber sehr selten und aus bestimmten Gründen. In diesem Fall ist das größte Problem, das ich haben würde, dass ich wahrscheinlich immer noch ein verwenden würde Action<SomeObjectType>, wodurch ich später zusätzliche Eigenschaften hinzufügen und gelegentlich die 2-Wege-Eigenschaft (Think Handledoder andere Feedback-Ereignisse, bei denen die Der Abonnent muss eine Eigenschaft für das Ereignisobjekt festlegen . Und wenn Sie einmal damit angefangen haben, können Sie es auch EventHandler<T>für einige verwenden T.

Marc Gravell
quelle
14

Der Vorteil eines wortreicheren Ansatzes ergibt sich, wenn sich Ihr Code in einem 300.000-Zeilen-Projekt befindet.

Wenn Sie die Aktion verwenden, wie Sie es getan haben, können Sie mir nicht sagen, was Bool, Int und Blah sind. Wenn Ihre Aktion ein Objekt übergeben hat, das die Parameter definiert hat, ist dies in Ordnung.

Wenn Sie einen EventHandler verwenden, der ein EventArgs haben möchte, und wenn Sie Ihr DiagnosticsArgs-Beispiel mit Gettern für die Eigenschaften vervollständigen würden, die ihren Zweck kommentiert haben, ist Ihre Anwendung verständlicher. Bitte kommentieren oder benennen Sie die Argumente im DiagnosticsArgs-Konstruktor vollständig.

Paul Matovich
quelle
6

Wenn Sie dem Standardereignismuster folgen, können Sie eine Erweiterungsmethode hinzufügen, um die Überprüfung des Auslösens von Ereignissen sicherer / einfacher zu machen. (dh der folgende Code fügt eine Erweiterungsmethode namens SafeFire () hinzu, die die Nullprüfung durchführt und (offensichtlich) das Ereignis in eine separate Variable kopiert, um vor der üblichen Null-Race-Bedingung, die Ereignisse beeinflussen kann, sicher zu sein.)

(Obwohl ich mir nicht sicher bin, ob Sie Erweiterungsmethoden für Nullobjekte verwenden sollten ...)

public static class EventFirer
{
    public static void SafeFire<TEventArgs>(this EventHandler<TEventArgs> theEvent, object obj, TEventArgs theEventArgs)
        where TEventArgs : EventArgs
    {
        if (theEvent != null)
            theEvent(obj, theEventArgs);
    }
}

class MyEventArgs : EventArgs
{
    // Blah, blah, blah...
}

class UseSafeEventFirer
{
    event EventHandler<MyEventArgs> MyEvent;

    void DemoSafeFire()
    {
        MyEvent.SafeFire(this, new MyEventArgs());
    }

    static void Main(string[] args)
    {
        var x = new UseSafeEventFirer();

        Console.WriteLine("Null:");
        x.DemoSafeFire();

        Console.WriteLine();

        x.MyEvent += delegate { Console.WriteLine("Hello, World!"); };
        Console.WriteLine("Not null:");
        x.DemoSafeFire();
    }
}
Paul Westcott
quelle
4
... können Sie mit Action <T> nicht dasselbe tun? SafeFire <T> (diese Aktion <T> theEvent, T theEventArgs) sollte funktionieren ... und es ist nicht erforderlich, "where" zu verwenden
Beachwalker
6

Mir ist klar, dass diese Frage über 10 Jahre alt ist, aber es scheint mir, dass nicht nur die offensichtlichste Antwort nicht angesprochen wurde, sondern dass aus der Frage möglicherweise nicht wirklich ein gutes Verständnis dafür hervorgeht, was unter der Decke vor sich geht. Darüber hinaus gibt es weitere Fragen zur verspäteten Bindung und was dies für Delegierte und Lambdas bedeutet (dazu später mehr).

Sprechen Sie zuerst den 800 Pfund schweren Elefanten / Gorilla im Raum an, wenn Sie eventvs Action<T>/ wählen möchten Func<T>:

  • Verwenden Sie ein Lambda, um eine Anweisung oder Methode auszuführen. Verwenden eventSie diese Option, wenn Sie mehr von einem Pub / Sub-Modell mit mehreren Anweisungen / Lambdas / Funktionen möchten, die ausgeführt werden (dies ist auf Anhieb ein großer Unterschied).
  • Verwenden Sie ein Lambda, wenn Sie Anweisungen / Funktionen zu Ausdrucksbäumen kompilieren möchten. Verwenden Sie Delegaten / Ereignisse, wenn Sie an einer traditionelleren Spätbindung teilnehmen möchten, wie sie beispielsweise in Reflection und COM Interop verwendet wird.

Lassen Sie uns als Beispiel für ein Ereignis eine einfache und standardmäßige Reihe von Ereignissen mithilfe einer kleinen Konsolenanwendung wie folgt verkabeln:

public delegate void FireEvent(int num);

public delegate void FireNiceEvent(object sender, SomeStandardArgs args);

public class SomeStandardArgs : EventArgs
{
    public SomeStandardArgs(string id)
    {
        ID = id;
    }

    public string ID { get; set; }
}

class Program
{
    public static event FireEvent OnFireEvent;

    public static event FireNiceEvent OnFireNiceEvent;


    static void Main(string[] args)
    {
        OnFireEvent += SomeSimpleEvent1;
        OnFireEvent += SomeSimpleEvent2;

        OnFireNiceEvent += SomeStandardEvent1;
        OnFireNiceEvent += SomeStandardEvent2;


        Console.WriteLine("Firing events.....");
        OnFireEvent?.Invoke(3);
        OnFireNiceEvent?.Invoke(null, new SomeStandardArgs("Fred"));

        //Console.WriteLine($"{HeightSensorTypes.Keyence_IL030}:{(int)HeightSensorTypes.Keyence_IL030}");
        Console.ReadLine();
    }

    private static void SomeSimpleEvent1(int num)
    {
        Console.WriteLine($"{nameof(SomeSimpleEvent1)}:{num}");
    }
    private static void SomeSimpleEvent2(int num)
    {
        Console.WriteLine($"{nameof(SomeSimpleEvent2)}:{num}");
    }

    private static void SomeStandardEvent1(object sender, SomeStandardArgs args)
    {

        Console.WriteLine($"{nameof(SomeStandardEvent1)}:{args.ID}");
    }
    private static void SomeStandardEvent2(object sender, SomeStandardArgs args)
    {
        Console.WriteLine($"{nameof(SomeStandardEvent2)}:{args.ID}");
    }
}

Die Ausgabe sieht wie folgt aus:

Geben Sie hier die Bildbeschreibung ein

Wenn Sie dasselbe mit Action<int>oder tun Action<object, SomeStandardArgs>würden, würden Sie nur SomeSimpleEvent2und sehen SomeStandardEvent2.

Also, was ist in dir los event?

Wenn wir erweitern FireNiceEvent, generiert der Compiler tatsächlich Folgendes (ich habe einige Details in Bezug auf die Thread-Synchronisation weggelassen, die für diese Diskussion nicht relevant sind):

   private EventHandler<SomeStandardArgs> _OnFireNiceEvent;

    public void add_OnFireNiceEvent(EventHandler<SomeStandardArgs> handler)
    {
        Delegate.Combine(_OnFireNiceEvent, handler);
    }

    public void remove_OnFireNiceEvent(EventHandler<SomeStandardArgs> handler)
    {
        Delegate.Remove(_OnFireNiceEvent, handler);
    }

    public event EventHandler<SomeStandardArgs> OnFireNiceEvent
    {
        add
        {
            add_OnFireNiceEvent(value)
        }
        remove
        {
            remove_OnFireNiceEvent(value)

        }
    }

Der Compiler generiert eine private Delegatenvariable, die für den Klassennamensraum, in dem sie generiert wird, nicht sichtbar ist. Dieser Delegat wird für das Abonnementmanagement und die verspätete Teilnahme verwendet, und die öffentlich zugängliche Oberfläche ist die vertraute +=und die -=Betreiber, die wir alle kennen und lieben gelernt haben :)

Sie können den Code für die Handler zum Hinzufügen / Entfernen anpassen, indem Sie den Bereich des FireNiceEventDelegaten in "Geschützt" ändern . Auf diese Weise können Entwickler den Hooks jetzt benutzerdefinierte Hooks hinzufügen, z. B. Protokollierungs- oder Sicherheitshooks. Dies führt zu einigen sehr leistungsstarken Funktionen, die jetzt einen benutzerdefinierten Zugriff auf Abonnements basierend auf Benutzerrollen usw. ermöglichen. Können Sie dies mit Lambdas tun? (Eigentlich können Sie Ausdrucksbäume benutzerdefiniert kompilieren, aber das geht über den Rahmen dieser Antwort hinaus).

Um einige Punkte aus einigen der Antworten hier anzusprechen:

  • Es gibt wirklich keinen Unterschied in der 'Sprödigkeit' zwischen dem Ändern der Args-Liste in Action<T>und dem Ändern der Eigenschaften in einer von abgeleiteten Klasse EventArgs. Beide erfordern nicht nur eine Kompilierungsänderung, sondern auch eine öffentliche Schnittstelle und eine Versionierung. Kein Unterschied.

  • In Bezug darauf, welcher Industriestandard ist, hängt dies davon ab, wo und warum dieser verwendet wird. Action<T>und solche werden häufig in IoC und DI verwendet und eventwerden häufig in Nachrichtenrouting wie GUI- und MQ-Frameworks verwendet. Beachten Sie, dass ich oft gesagt habe , nicht immer .

  • Delegierte haben andere Lebensdauern als Lambdas. Man muss sich auch der Gefangennahme bewusst sein ... nicht nur mit dem Schließen, sondern auch mit dem Gedanken "Schau, was die Katze hineingezogen hat". Dies wirkt sich sowohl auf den Speicherbedarf / die Lebensdauer als auch auf die Verwaltung aus.

Eine weitere Sache, auf die ich bereits hingewiesen habe ... der Begriff der späten Bindung. Sie werden dies häufig sehen, wenn Sie ein Framework wie LINQ verwenden, wenn ein Lambda "live" wird. Dies unterscheidet sich stark von der späten Bindung eines Delegierten, die mehr als einmal auftreten kann (dh das Lambda ist immer vorhanden, die Bindung erfolgt jedoch bei Bedarf so oft wie erforderlich), im Gegensatz zu einem Lambda, das, sobald es auftritt, durchgeführt wird - Die Magie ist verschwunden und die Methode (n) / Eigenschaft (en) werden immer gebunden. Etwas zu beachten.

Stacy Dudovitz
quelle
4

Mit Blick auf Standard .NET-Ereignismuster finden wir

Die Standardsignatur für einen .NET-Ereignisdelegierten lautet:

void OnEventRaised(object sender, EventArgs args);

[...]

Die Argumentliste enthält zwei Argumente: das Absender- und das Ereignisargument. Der Kompilierzeittyp des Absenders ist System.Object, obwohl Sie wahrscheinlich einen abgeleiteten Typ kennen, der immer korrekt wäre. Verwenden Sie gemäß Konvention das Objekt .

Unten auf derselben Seite finden wir ein Beispiel für die typische Ereignisdefinition, die so etwas wie ist

public event EventHandler<EventArgs> EventName;

Hatten wir definiert

class MyClass
{
  public event Action<MyClass, EventArgs> EventName;
}

der Handler hätte sein können

void OnEventRaised(MyClass sender, EventArgs args);

wo senderhat den richtigen ( mehr abgeleiteten ) Typ.

user1832484
quelle
Es tut uns leid, nicht bemerkt zu haben, dass der Unterschied in der Handlersignatur liegt, was von einer genaueren Eingabe profitieren würde sender.
user1832484