evento Azione <> vs evento EventHandler <>


144

C'è qualche differenza tra dichiarare event Action<>e event EventHandler<>.

Supponendo che non importa quale oggetto abbia effettivamente generato un evento.

per esempio:

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

vs

public event EventHandler<DiagnosticsArgs> DiagnosticsEvent;

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

l'utilizzo sarebbe quasi lo stesso in entrambi i casi:

obj.DiagnosticsEvent += HandleDiagnosticsEvent;

Ci sono diverse cose che non mi piacciono per lo event EventHandler<>schema:

  • Dichiarazione di tipo extra derivata da EventArgs
  • Passaggio obbligatorio della fonte dell'oggetto - spesso a nessuno importa

Più codice significa più codice da mantenere senza alcun chiaro vantaggio.

Di conseguenza, preferisco event Action<>

Tuttavia, solo se nell'Azione <> sono presenti troppi argomenti di tipo, sarebbe necessaria una classe aggiuntiva.


2
plusOne (ho appena battuto il sistema) per "a nessuno importa"
hyankov,

@plusOne: ho davvero bisogno di conoscere il mittente! Di 'che succede qualcosa e vuoi sapere chi è stato. Ecco dove hai bisogno di 'object source' (aka mittente).
Kamran Bigdely,

il mittente può essere una proprietà nel payload dell'evento
Thanasis Ioannidis,

Risposte:


67

La differenza principale sarà che se usi il Action<>tuo evento non seguirai il modello di progettazione praticamente di qualsiasi altro evento nel sistema, che considererei uno svantaggio.

Un aspetto positivo del modello di progettazione dominante (a parte il potere dell'identità) è che puoi estendere l' EventArgsoggetto con nuove proprietà senza alterare la firma dell'evento. Questo sarebbe ancora possibile se lo avessi usato Action<SomeClassWithProperties>, ma non vedo davvero il punto di non usare l'approccio regolare in quel caso.


È possibile utilizzare il Action<>risultato in perdite di memoria? Un aspetto negativo del EventHandlermodello di progettazione sono le perdite di memoria. Inoltre, va sottolineato che possono esserci più gestori di eventi ma una sola azione
Luke T O'Brien,

4
@ LukeTO'Brien: gli eventi sono in sostanza delegati, quindi esistono le stesse possibilità di perdita di memoria Action<T>. Inoltre, è Action<T> possibile fare riferimento a diversi metodi. Ecco un riassunto che lo dimostra: gist.github.com/fmork/4a4ddf687fa8398d19ddb2df96f0b434
Fredrik Mörk,

89

Sulla base di alcune delle risposte precedenti, suddividerò la mia risposta in tre aree.

In primo luogo, i limiti fisici dell'uso Action<T1, T2, T2... >vs usando una classe derivata di EventArgs. Ci sono tre: in primo luogo, se si modifica il numero o i tipi di parametri, ogni metodo a cui si abbonerà dovrà essere modificato per conformarsi al nuovo modello. Se si tratta di un evento pubblico che verranno utilizzati da assiemi di terze parti e vi è la possibilità che l'evento args cambierebbe, questo sarebbe un motivo per usare una classe personalizzata derivata dagli argomenti dell'evento per motivi di coerenza (ricordate, POTREBBE comunque usa a Action<MyCustomClass>) In secondo luogo, l'utilizzo Action<T1, T2, T2... >ti impedirà di passare il feedback INDIETRO al metodo chiamante a meno che tu non abbia un qualche tipo di oggetto (con una proprietà Gestita per esempio) che viene passato insieme all'Azione. Terzo, non ottieni parametri nominati, quindi se passi 3 boolè un int, duestringE, a DateTime, non hai idea di quale sia il significato di questi valori. Come nota a margine, puoi comunque avere un "Metodo sicuro di questo evento mentre stai ancora usando Action<T1, T2, T2... >".

In secondo luogo, implicazioni sulla coerenza. Se hai già un sistema di grandi dimensioni con cui stai già lavorando, è quasi sempre meglio seguire il modo in cui è progettato il resto del sistema, a meno che tu non abbia una buona ragione, non troppo. Se hai eventi pubblici che devono essere mantenuti, la capacità di sostituire le classi derivate può essere importante. Tienilo a mente.

In terzo luogo, nella pratica della vita reale, trovo personalmente che tendo a creare molti eventi una tantum per cose come i cambiamenti di proprietà con i quali ho bisogno di interagire (in particolare quando faccio MVVM con modelli di vista che interagiscono tra loro) o dove l'evento ha un singolo parametro. Il più delle volte questi eventi assumono la forma di public event Action<[classtype], bool> [PropertyName]Changed;o public event Action SomethingHappened;. In questi casi, ci sono due vantaggi. Innanzitutto, ottengo un tipo per la classe emittente. Se MyClassdichiara ed è l'unica classe che attiva l'evento, ottengo un'istanza esplicita MyClasscon cui lavorare nel gestore dell'evento. In secondo luogo, per eventi semplici come gli eventi di modifica delle proprietà, il significato dei parametri è ovvio e dichiarato nel nome del gestore dell'evento e non devo creare una miriade di classi per questo tipo di eventi.


Fantastico post sul blog. Sicuramente merita una lettura se stai leggendo questa discussione!
Vexir,

1
Risposta dettagliata e ben ponderata che spiega il ragionamento alla base della conclusione
MikeT

18

Nella maggior parte dei casi, direi di seguire lo schema. Mi sono deviato da esso, ma molto raramente e per motivi specifici. Nel caso in questione, il problema più grande che avrei è che probabilmente avrei ancora usato un Action<SomeObjectType>, permettendomi di aggiungere proprietà extra in seguito e di utilizzare la proprietà a 2 vie occasionale (pensa Handled, o altri eventi di feedback in cui il l'abbonato deve impostare una proprietà sull'oggetto evento). E una volta che hai iniziato su quella linea, potresti anche usarlo EventHandler<T>per alcuni T.


14

Il vantaggio di un approccio più complesso si ha quando il tuo codice è all'interno di un progetto di 300.000 righe.

Usando l'azione, come hai fatto, non c'è modo di dirmi cosa sono bool, int e Blah. Se la tua azione ha superato un oggetto che ha definito i parametri, allora ok.

Utilizzando un EventHandler che desiderava un EventArgs e se si completasse l'esempio DiagnosticsArgs con getter per le proprietà che hanno commentato il loro scopo, l'applicazione sarebbe più comprensibile. Inoltre, si prega di commentare o dare un nome completo agli argomenti nel costruttore DiagnosticsArgs.


6

Se segui il modello di eventi standard, puoi aggiungere un metodo di estensione per rendere più sicuro / facile il controllo dell'attivazione degli eventi. (ovvero il codice seguente aggiunge un metodo di estensione chiamato SafeFire () che esegue il controllo null, nonché (ovviamente) la copia dell'evento in una variabile separata per essere al sicuro dalla solita condizione di razza nulla che può influenzare gli eventi.)

(Anche se ho due menti se dovresti usare metodi di estensione su oggetti null ...)

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

4
... non puoi fare lo stesso con Action <T>? SafeFire <T> (questa azione <T> theEvent, T theEventArgs) dovrebbe funzionare su ... e non è necessario utilizzare "where"
Beachwalker

6

Mi rendo conto che questa domanda ha più di 10 anni, ma mi sembra che non solo la risposta più ovvia non sia stata affrontata, ma che forse non è davvero chiaro dalla domanda una buona comprensione di ciò che accade sotto le coperte. Inoltre, ci sono altre domande sull'associazione tardiva e sul significato dei delegati e degli lambda (ne parleremo più avanti).

Prima di affrontare l'elefante / gorilla da 800 libbre nella stanza, quando scegliere eventvs Action<T>/ Func<T>:

  • Utilizzare un lambda per eseguire un'istruzione o un metodo. Utilizzare eventquando si desidera più di un pub / modello secondario con più istruzioni / lambdas / funzioni che verranno eseguite (questa è una grande differenza fin dall'inizio).
  • Utilizzare un lambda quando si desidera compilare istruzioni / funzioni in alberi di espressioni. Utilizzare i delegati / eventi quando si desidera partecipare a più tardi l'associazione tardiva tradizionale come quella utilizzata nella riflessione e l'interoperabilità COM.

Come esempio di un evento, consente di collegare un set di eventi semplice e "standard" utilizzando una piccola applicazione console come segue:

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

L'output avrà il seguente aspetto:

inserisci qui la descrizione dell'immagine

Se hai fatto lo stesso con Action<int> o Action<object, SomeStandardArgs>, vedresti solo SomeSimpleEvent2e SomeStandardEvent2.

Cosa sta succedendo dentro event?

Se ci espandiamo FireNiceEvent , il compilatore sta effettivamente generando quanto segue (ho omesso alcuni dettagli riguardo alla sincronizzazione dei thread che non sono rilevanti per questa discussione):

   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)

        }
    }

Il compilatore genera una variabile delegata privata che non è visibile nello spazio dei nomi della classe in cui viene generata. Quel delegato è ciò che viene utilizzato per la gestione delle iscrizioni e la partecipazione vincolante in ritardo, e l'interfaccia pubblica è familiare+= e-= operatori che tutti abbiamo imparato a conoscere e ad amare:)

È possibile personalizzare il codice per i gestori Aggiungi / Rimuovi modificando l'ambito di FireNiceEvent delegato in protetto. Ciò ora consente agli sviluppatori di aggiungere hook personalizzati agli hook, come la registrazione o gli hook di sicurezza. Questo rende davvero alcune funzionalità molto potenti che ora consentono un'accessibilità personalizzata alla sottoscrizione in base ai ruoli degli utenti, ecc. Puoi farlo con lambdas? (In realtà puoi compilare alberi delle espressioni personalizzati, ma questo va oltre lo scopo di questa risposta).

Per rispondere a un paio di punti di alcune delle risposte qui:

  • Non c'è davvero alcuna differenza nella "fragilità" tra la modifica dell'elenco args Action<T>e la modifica delle proprietà in una classe derivata EventArgs. Entrambi non solo richiedono una modifica della compilazione, ma cambieranno anche un'interfaccia pubblica e richiederanno il controllo delle versioni. Nessuna differenza.

  • Per quanto riguarda lo standard industriale, dipende da dove viene utilizzato e perché. Action<T>e tale viene spesso utilizzato in IoC e DI, e eventviene spesso utilizzato nel routing dei messaggi come i framework di tipo GUI e MQ. Si noti che ho detto spesso , non sempre .

  • I delegati hanno vite diverse rispetto a lambda. Bisogna anche essere consapevoli della cattura ... non solo con la chiusura, ma anche con la nozione di "guarda cosa trascinava il gatto". Ciò influisce sul footprint / durata della memoria e sulle perdite di gestione.

Ancora una cosa, qualcosa a cui ho fatto riferimento in precedenza ... la nozione di associazione tardiva. Lo vedrai spesso quando usi framework come LINQ, riguardo a quando un lambda diventa 'live'. Questo è molto diverso dall'associazione tardiva di un delegato, che può avvenire più di una volta (cioè la lambda è sempre lì, ma l'associazione si verifica su richiesta tutte le volte che è necessario), al contrario di una lambda, che una volta che si verifica, viene eseguita - la magia è sparita e il metodo (i) / proprietà (e) si legheranno sempre. Qualcosa da tenere a mente.


4

Osservando i modelli di eventi .NET standard che troviamo

La firma standard per un delegato di eventi .NET è:

void OnEventRaised(object sender, EventArgs args);

[...]

L'elenco degli argomenti contiene due argomenti: il mittente e gli argomenti dell'evento. Il tipo di tempo di compilazione del mittente è System.Object, anche se probabilmente conosci un tipo più derivato che sarebbe sempre corretto. Per convenzione, usa l' oggetto .

Di seguito nella stessa pagina troviamo un esempio della definizione tipica dell'evento che è qualcosa di simile

public event EventHandler<EventArgs> EventName;

Avevamo definito

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

il gestore avrebbe potuto essere

void OnEventRaised(MyClass sender, EventArgs args);

dove senderha il tipo corretto ( più derivato ).


Ci dispiace non aver notato che la differenza sta nella firma del gestore, che trarrebbe vantaggio da una digitazione più precisa sender .
user1832484
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.