Menu

Eventi in C#: parola chiave event, EventHandler, sottoscrizione e generazione

Come funzionano gli eventi in C#: dichiarare un evento con EventHandler e EventHandler<T>, classi EventArgs personalizzate, sottoscrivere e annullare la sottoscrizione con += e -=, generare un evento in sicurezza con ?.Invoke, perché un evento è meglio di un campo delegate pubblico e il memory leak causato da una sottoscrizione dimenticata.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Un evento permette a una classe di annunciare che è successo qualcosa senza sapere chi sta ascoltando. Gli altri oggetti si sottoscrivono con +=, e quando la classe genera l'evento, viene eseguito il metodo di ogni sottoscrittore.

Output:

Order A-17 saved
  receipt emailed for A-17
Order A-18 saved
  receipt emailed for A-18
  free gift added to A-18

Shop non sa che esistono ricevute o regali. Genera OrderPlaced, e chiunque si sia sottoscritto reagisce. Aggiungere una nuova reazione (punti fedeltà, analytics) significa aggiungere un sottoscrittore, non modificare PlaceOrder.

Dichiarare un evento

La dichiarazione di un evento è un membro di tipo delegate con la parola chiave event:

public event EventHandler<OrderPlacedEventArgs> OrderPlaced;

Il tipo delegate fissa la firma dell'handler. Il codice .NET usa quasi sempre uno di due tipi predefiniti, e seguire questa convenzione rende i tuoi eventi familiari a qualsiasi altro sviluppatore .NET:

  • EventHandler: void (object sender, EventArgs e), per gli eventi che non trasportano dati. Generalo con EventArgs.Empty.
  • EventHandler<TEventArgs>: void (object sender, TEventArgs e), dove TEventArgs è la tua classe che trasporta i dati.

Le convenzioni che li accompagnano:

  • sender è l'oggetto che ha generato l'evento, di solito this.
  • La classe dei dati si chiama {Something}EventArgs, deriva da EventArgs (facoltativo da .NET 4.5, ma ancora consuetudine) ed espone proprietà di sola lettura.
  • L'evento prende il nome da ciò che è successo: OrderPlaced, Closed, PriceChanged. Un nome in ...ing (Closing) si usa per gli eventi generati prima dell'azione, spesso con un modo per annullarla.
  • Nelle classi pensate per essere ereditate, la generazione passa da un metodo protected virtual void OnOrderPlaced(OrderPlacedEventArgs e), così le classi derivate possono intervenire.

Sottoscrivere e annullare la sottoscrizione

+= aggiunge un handler e -= lo rimuove. Un handler può essere un metodo o una lambda. Per rimuovere una lambda in seguito, tienila in una variabile, perché scrivere di nuovo la stessa lambda crea un delegate diverso che -= non troverà:

Output:

set to 32
  display shows 32 C
  alarm: too hot
set to 35
  display shows 35 C
set to 20
  display shows 20 C

Rimuovere un handler mai aggiunto non è un errore; semplicemente non fa nulla. EventHandler<int> mostra che sul .NET moderno l'argomento di tipo non deve per forza derivare da EventArgs, anche se una classe dedicata lascia spazio per aggiungere campi in seguito senza rompere i sottoscrittori.

Gli handler vengono eseguiti in modo sincrono, nell'ordine di sottoscrizione, sul thread che ha generato l'evento. Se un handler lancia un'eccezione, gli handler restanti non vengono eseguiti e l'eccezione si propaga al codice che ha generato l'evento.

Generare un evento in sicurezza

Un evento senza sottoscrittori contiene null. Chiamarlo direttamente lancerebbe NullReferenceException, quindi generalo con l'operatore null-condizionale:

OrderPlaced?.Invoke(this, args);

Prima di C# 6 il pattern consisteva nel copiare il campo in una variabile locale, controllarla e invocare la copia. La copia conta nel codice multithread: controllare OrderPlaced != null e poi chiamare OrderPlaced(...) legge il campo due volte, e nel frattempo un altro thread potrebbe rimuovere l'ultimo handler. ?. lo legge una volta sola, quindi è sia più breve sia corretto.

Perché event e non un campo delegate pubblico

Senza la parola chiave event, un campo delegate pubblico funziona per la sottoscrizione, ma dà a ogni sottoscrittore il pieno controllo:

Output:

analytics ping
analytics ping

Un solo = scritto al posto di += ha rimosso silenziosamente i due handler precedenti, e il codice esterno ha potuto generare il "click" senza alcun click. Marcare il campo come event rende entrambe le righe errori di compilazione:

error CS0070: The event 'Button.Clicked' can only appear on the left hand side of += or -= (except when used from within the type 'Button')

Dentro Button, l'evento si comporta ancora come un normale campo delegate, quindi la classe può invocarlo e controllare se è null.

Il leak della sottoscrizione dimenticata

Quando ti sottoscrivi, l'evento memorizza un delegate, e il delegate contiene un riferimento all'oggetto sottoscrittore. Finché il publisher è vivo e l'handler è collegato, il sottoscrittore non può essere raccolto dal garbage collector e continua a ricevere eventi anche dopo che hai finito di usarlo:

Output:

  closed widget shows 101.5
  open widget shows 101.5
subscribers left: 1
  closed widget shows 99.0

Impostare leaky a null non ha avuto alcun effetto sul feed: la sua lista di handler fa ancora riferimento a quel widget, quindi il widget "chiuso" continua ad aggiornarsi e resta in memoria. Il widget nel blocco using ha annullato la sottoscrizione in Dispose e ha smesso di ricevere. È uno dei memory leak più comuni nel codice .NET desktop e server, soprattutto con gli eventi statici e i servizi a livello di applicazione, che vivono fino alla fine del processo.

La regola: chi si sottoscrive a un evento di un oggetto che vive più a lungo di lui deve annullare la sottoscrizione, di solito in Dispose (vedi l'istruzione using). Quando publisher e sottoscrittore hanno la stessa durata, come un form e i suoi pulsanti, non serve annullare la sottoscrizione.

Accessor add e remove personalizzati

Un evento può definire cosa fanno += e -=, come il get e il set di una proprietà. È raro, ma è così che i framework memorizzano gli handler in un dizionario condiviso o li inoltrano a un altro oggetto:

private EventHandler closed;

public event EventHandler Closed
{
    add    { Console.WriteLine("subscriber added");   closed += value; }
    remove { Console.WriteLine("subscriber removed"); closed -= value; }
}

Con gli accessor personalizzati, la classe genera l'evento tramite il campo di appoggio (closed?.Invoke(this, EventArgs.Empty)), dato che l'evento in sé non ha più una propria memoria.

Domande frequenti

Cos'è un evento in C#?

Un evento è un membro di una classe che permette ad altri oggetti di chiedere di essere avvisati quando succede qualcosa. Si basa su un delegate multicast, ma il codice esterno può solo aggiungere handler (+=) e rimuoverli (-=). Solo la classe che dichiara l'evento può generarlo.

Che differenza c'è tra un evento e un delegate in C#?

Un delegate è un tipo che fa riferimento a metodi. Un evento è un membro dichiarato con un tipo delegate più la parola chiave event, che limita l'accesso: dall'esterno della classe non puoi invocarlo, leggere la sua lista di handler né assegnarlo con =, puoi solo sottoscriverti e annullare la sottoscrizione. Un campo delegate pubblico permette tutto questo, quindi qualsiasi sottoscrittore potrebbe cancellare gli altri o generare l'evento.

Come passo dati con un evento C#?

Deriva una classe da EventArgs con i dati come proprietà di sola lettura (public class OrderPlacedEventArgs : EventArgs { public decimal Total { get; } }) e dichiara l'evento come EventHandler<OrderPlacedEventArgs>. Generalo con OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(total));.

Come genero un evento in sicurezza in C#?

Usa MyEvent?.Invoke(this, args);. Un evento senza sottoscrittori è null, quindi chiamarlo direttamente lancia NullReferenceException. L'operatore ?. legge il campo una sola volta, il che evita anche una race condition in cui un altro thread rimuove l'ultimo handler tra un controllo su null e la chiamata.

Gli eventi C# possono causare memory leak?

Sì. La sottoscrizione memorizza un delegate che fa riferimento al sottoscrittore, quindi il publisher tiene in vita il sottoscrittore per tutto il tempo in cui vive il publisher stesso. Un publisher di lunga durata (un evento statico, un servizio a livello di applicazione) con sottoscrittori di breve durata che non annullano mai la sottoscrizione li tiene tutti in memoria. Annulla la sottoscrizione con -= quando il sottoscrittore ha finito, di solito in Dispose.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA