1. Introduzione
Lavorando con i programmi, inevitabilmente ti trovi in situazioni in cui una parte deve «informare» le altre che è successo qualcosa di importante. L'esempio classico — l'utente ha cliccato con il mouse, e quell'evento richiede gestione. Nella vita di tutti i giorni viviamo già nel mondo degli eventi: la kettlera in cucina fischia — hai sentito il segnale e hai spento i fornelli. Il caffè è caduto sulla tastiera — il cuore ha fatto un balzo — e sei corso a salvare il laptop. La programmazione segue le stesse regole.
Evento — è un meccanismo che permette a un oggetto-sorgente (publisher) di avvisare altri oggetti (subscriber) di cambiamenti o azioni avvenute. È una specie di «ho segnalato — chi ha ascoltato risponde».
In C# gli eventi sono una costruzione speciale basata su un tipo delegate. Il delegate definisce la signature della callback — cosa e come verrà chiamato nei subscriber. La dichiarazione di un evento si fa con la keyword event, e il delegate — con la keyword delegate.
Perché servono gli eventi?
- Coupling debole: il publisher non sa nulla sui subscriber — invia solo il segnale.
- Flessibilità dell'architettura: puoi aggiungere e rimuovere handler dinamicamente senza cambiare il codice del publisher.
- Scalabilità: aggiungi un nuovo subscriber — e inizia subito a ricevere notifiche.
Il classico pattern «Publisher-Subscriber»
Immaginiamo di avere una classe «Allarme antincendio» (publisher) e una classe «Persona nell'edificio» (subscriber). Quando l'allarme suona, invia il segnale a tutti contemporaneamente — non importa quante persone ci siano nell'edificio o dove si trovino. Questo è il pattern «Publisher-Subscriber» (o Observer).
Il publisher non sa quanti e quali subscriber esistono — semplicemente notifica, e gli altri si iscrivono alle notifiche o le ignorano.
Come funziona in C#?
- Publisher: definisce l'evento (event), fornisce sottoscrizione/cancellazione.
- Subscriber: si iscrive all'evento e implementa un handler (il metodo-handler verrà chiamato quando l'evento accade).
2. Eventi in pratica: primo esempio
Passando dalla teoria al codice, descriviamo un modello minimale. Supponiamo di avere un'app console dove un oggetto-timer «ticca» ogni secondo, e diversi handler reagiscono (per esempio, stampano «tic» sulla console o contano il numero di tic).
Passo 1. Definiamo il delegate e l'evento
public class SimpleTimer
{
// Dichiariamo il delegate per l'evento
public delegate void TickEventHandler(object sender, EventArgs e);
// Evento basato sul delegate
public event TickEventHandler Tick;
public void Start(int count)
{
for (int i = 0; i < count; i++)
{
System.Threading.Thread.Sleep(1000); // simulazione del tic!
OnTick(); // scatenare l'evento!
}
}
protected virtual void OnTick()
{
// Chiamiamo l'evento, se ci sono subscriber (Tick != null)
Tick?.Invoke(this, EventArgs.Empty);
}
}
Cosa succede qui?
- È definito il delegate TickEventHandler con la signature classica object sender, EventArgs e.
- L'evento Tick — è il punto di sottoscrizione per gli handler.
- Il metodo Start simula il «tic» e chiama periodicamente OnTick.
- In OnTick l'evento viene chiamato in modo sicuro: Tick?.Invoke(..., EventArgs.Empty).
Passo 2. Iscrizione all'evento
class Program
{
static void Main()
{
var timer = new SimpleTimer();
// Ci iscriviamo all'evento Tick
timer.Tick += Timer_Tick;
timer.Start(3);
// Possiamo cancellare l'iscrizione, se serve
timer.Tick -= Timer_Tick;
}
static void Timer_Tick(object sender, EventArgs e)
{
Console.WriteLine("Tic!");
}
}
Creiamo il timer, ci iscriviamo con l'operatore +=, poi ad ogni tic viene chiamato l'handler. La cancellazione usa l'operatore -=.
3. Dettagli utili
Perché gli eventi sono preferibili rispetto a chiamate dirette?
Se SimpleTimer dentro OnTick scrivesse direttamente sulla console, la classe sarebbe rigidamente accoppiata a quell'azione concreta. Gli eventi «liberano» il codice: il timer non sa cosa faranno i subscriber — lanciare un razzo, loggare su file o inviare una e-mail.
Importante differenza tra eventi e delegate
- Delegate — è un «puntatore» a un metodo, mentre evento — è un delegate con restrizioni di accesso.
- I subscriber possono solo iscriversi/annullare l'iscrizione; chiamare l'evento dall'esterno non è permesso — solo il publisher può farlo.
- Per dichiarare un evento, aggiungi il modificatore event al tipo delegate — il compilatore garantirà il modello di accesso corretto.
Schema sintetico del funzionamento di un evento in C#
+------------------+ +------------------------------+
| | | |
| Publisher | <------> | Subscriber |
| (Publisher/Event)| | (Subscriber/Handler) |
| | | |
+------------------+ +------------------------------+
| 1) dichiara evento | 2) si iscrive ad esso
| 3) lo invoca | 4) implementa l'handler
Quando usare gli eventi?
- Quando devi notificare un numero indefinito di listener su qualcosa che è successo.
- Quando non vuoi legare la logica delle azioni dentro la classe sorgente.
- UI, interazioni asincrone, notifiche di sistema — tutto ruota intorno agli eventi.
Brevi caratteristiche dell'implementazione degli eventi in C#
- Un evento non può essere invocato «dall'esterno»: solo il codice del publisher ha il diritto di chiamare Invoke.
- Sottoscrizione/cancellazione: operatori +=/-=; possono esserci più handler.
- Un evento è fondamentalmente una lista di delegate: quando l'evento scatta, vengono chiamati tutti gli handler nell'ordine di sottoscrizione.
- Delegate raccomandati: usa EventHandler e EventHandler<TEventArgs> per compatibilità con l'ecosistema .NET.
4. Errori tipici dei principianti
Dimenticare di controllare se ci sono subscriber: Tick != null. Meglio usare la chiamata sicura: Tick?.Invoke(...).
Iscriversi a un evento e non cancellare l'iscrizione quando l'handler non è più necessario. Questo può tenere oggetti in memoria e causare memory leak.
Provare a «invocare» un evento da una classe esterna — il compilatore non lo permetterà. Non puoi scrivere qualcosa come game.GameOver(), se si tratta di un evento e non di un metodo.
Non rispettare la signature del delegate. Per gli eventi usa gli standard EventHandler o EventHandler<TEventArgs> — così il codice resta compatibile con le altre librerie .NET.
GO TO FULL VERSION