1. Introduzione
Lavorare con delegate ed eventi in C# è piacevole e comodo — il linguaggio fa molto per te. Tuttavia dietro questa facciata ci sono molte insidie: memory leak non evidenti, bug strani dovuti a doppie iscrizioni, desincronizzazione degli handler e persino eccezioni improvvise durante la notifica. Delegate ed eventi hanno potenza, ma richiedono attenzione al ciclo di vita degli oggetti, comprensione dei thread e sapere esattamente come funzionano le chiamate e le cancellazioni. Se i tuoi eventi funzionano "quasi sempre", ma a volte non partono o causano errori strani — non sei il solo! Vediamo dove anche programmatori esperti inciampano più spesso e come evitarlo.
Tabella degli errori principali
| Errore | Conseguenza | Come evitare |
|---|---|---|
| Doppia iscrizione | L'handler viene chiamato più volte | Tenere traccia delle iscrizioni, rimuovere prima di aggiungere |
| Mancata cancellazione (memory leak) | Subscriber rimangono in memoria, "zombie" | Rimuovere sempre le iscrizioni, usare IDisposable |
| Eccezione in un handler | Gli altri handler non verranno chiamati | try/catch negli handler o iterare manualmente |
| Modifica degli iscritti durante l'evento | Salti o chiamate duplicate | Iterare su una copia della lista di handler (GetInvocationList()) |
| Chiamata evento quando MyEvent == null | NullReferenceException | Controllare null, usare ?.Invoke |
| Firma dell'handler non corrispondente | Errore di compilazione | Verificare la firma |
| Chiamare evento dall'esterno | Errore di compilazione | Chiamare solo tramite OnEventName |
| Eventi statici fuori posto | Miscele di iscrizioni | Non rendere static senza motivo valido |
| Problemi di closure con lambda | Valori inattesi | Fare una copia della variabile |
2. Iscrizione multipla e chiamate multiple
Essenza dell'errore
Se ti iscrivi più volte lo stesso handler allo stesso evento, ogni operazione con += aggiunge il metodo alla coda del delegate. Di conseguenza l'handler verrà chiamato tante volte quante è stato aggiunto.
Come si manifesta?
Immagina di avere un pulsante e un handler per il click:
Button btn = new Button();
btn.Click += OnButtonClick; // Ci iscriviamo
btn.Click += OnButtonClick; // Ecco un'iscrizione duplicata!
Ora ad ogni click il metodo OnButtonClick verrà chiamato due volte. Se dentro l'handler aggiorni un contatore o aggiungi una voce al log, vedrai risultati raddoppiati.
Come correggerlo?
La doppia iscrizione di solito nasce da struttura del codice sbagliata — per esempio se += è in un metodo chiamato più volte (in diversi momenti del lifecycle della form).
- Controlla dove avvengono le iscrizioni.
- Non mettere += in fasi del lifecycle che possono essere richiamate ripetutamente.
- Talvolta è utile assicurarsi che l'iscrizione sia "unica" — prima di aggiungere rimuovi l'handler:
myEvent -= MyHandler; // Per sicurezza rimuoviamo
myEvent += MyHandler; // Poi ci iscriviamo di nuovo
Questo è sicuro: se l'handler non era presente, -= non farà nulla.
3. Subscriber "zombie"
Essenza dell'errore
Se un subscriber si iscrive ad un publisher con vita lunga e non si disiscrive, il garbage collector non potrà raccoglierlo: il publisher mantiene il riferimento al delegate dell'handler e quindi all'intero oggetto subscriber. Ne risulta un leak di memoria.
Esempio tipico
public class TemporaryPopup : IDisposable
{
private Window _hostWindow;
public TemporaryPopup(Window window)
{
_hostWindow = window;
_hostWindow.Closed += OnHostClosed;
}
private void OnHostClosed(object sender, EventArgs e)
{
// ...
}
public void Dispose()
{
_hostWindow.Closed -= OnHostClosed; // Non dimenticare di disiscriverti!
}
}
Se dimentichi Dispose() o non lo chiami, anche cancellando tutte le altre referenze a TemporaryPopup, l'oggetto non verrà distrutto — la window tiene ancora il riferimento al suo handler.
Come evitarlo?
- Implementa IDisposable nei subscriber se il loro ciclo di vita è più breve del publisher.
- Usa il pattern using o chiama esplicitamente Dispose():
using (var popup = new TemporaryPopup(mainWindow))
{
// ...
} // Qui Dispose verrà chiamato automaticamente
Nelle applicazioni GUI — disiscriviti negli handler di chiusura della finestra/form (per esempio in Dispose() della form).
4. Gestire eccezioni dentro gli handler
Essenza dell'errore
Quando un evento chiama decine di handler, e uno di questi lancia un'eccezione, gli altri handler non verranno eseguiti.
Dimostrazione
public event EventHandler MyEvent;
public void Raise()
{
MyEvent?.Invoke(this, EventArgs.Empty);
}
Se in uno dei metodi iscritti a MyEvent si verifica un'eccezione, la catena si interrompe e gli altri non verranno chiamati.
Come gestire questi casi?
- Negli handler o gestisci localmente le eccezioni (try/catch), oppure rilasciale consapevolmente.
- In scenari complessi — itera manualmente gli iscritti e isola gli errori di ciascuno:
var handlers = MyEvent?.GetInvocationList();
foreach (var handler in handlers)
{
try
{
handler.DynamicInvoke(this, EventArgs.Empty);
}
catch (Exception ex)
{
// Logging, recovery
}
}
5. Modifica della lista degli iscritti durante la notifica
Essenza dell'errore
Se un handler durante l'esecuzione si disiscrive o disiscrive altri, questo può alterare l'ordine delle chiamate: alcuni handler potrebbero essere saltati o chiamati due volte.
Come evitarlo?
- Non modificare le iscrizioni dagli handler.
- Se necessario — itera su una copia della lista di delegate:
var handlers = MyEvent?.GetInvocationList();
foreach (EventHandler handler in handlers)
{
handler(this, EventArgs.Empty);
}
6. Confusione con valore null del delegate (nessun iscritto)
Essenza dell'errore
Se a un evento non è iscritto nessuno, il suo delegate è null. Chiamarlo senza controllo causa NullReferenceException.
Esempio (sbagliato)
public event EventHandler MyEvent;
public void Raise()
{
MyEvent(this, EventArgs.Empty); // Se non ci sono iscritti — eccezione!
}
Come farlo correttamente?
- Usa la chiamata sicura: MyEvent?.Invoke(this, EventArgs.Empty).
- Oppure usa la tecnica classica thread-safe: copia il delegate in una variabile locale e chiamala.
7. Mescolare delegate/eventi con firme diverse
Essenza dell'errore
I delegate sono fortemente tipizzati. Una firma dell'handler che non corrisponde a quella dell'evento causa errore di compilazione.
Esempio
public event EventHandler<string> TextChanged;
void WrongHandler(object sender, int number) { /* ... */ }
TextChanged += WrongHandler; // Errore di compilazione!
Usa i delegate standard EventHandler e EventHandler<T>, e assicurati che le firme coincidano esattamente.
8. Tentare di chiamare un evento fuori dalla classe-publisher
Essenza dell'errore
event è un delegate incapsulato: il codice esterno può solo aggiungere/rimuovere handler, non invocarlo direttamente.
Esempio
public class MyPublisher
{
public event EventHandler SomethingHappened;
}
var publisher = new MyPublisher();
publisher.SomethingHappened?.Invoke(publisher, EventArgs.Empty); // Errore!
Come fare correttamente?
La chiamata dovrebbe avvenire tramite un metodo protetto/pubblico del publisher — tipicamente OnEventName. Dall'esterno si può solo fare +=/-=.
9. Errori con gli accessor add e remove
Essenza dell'errore
Accessor custom per eventi permettono di controllare l'iscrizione, ma è facile rompere la correttezza della coda di chiamata o la safety rispetto ai thread.
Esempio
public event EventHandler MyEvent
{
add { /* ... */ }
remove { /* ... */ }
}
Se non sei sicuro — usa l'implementazione standard degli eventi. Se gestisci manualmente, tieni a portata di mano la documentazione e cura la sincronizzazione.
10. Accesso a eventi da contesti statici e non statici
Essenza dell'errore
È facile dichiarare per errore un evento static dove dovrebbe essere per istanza. In quel caso tutti gli oggetti condivideranno la stessa coda di iscritti.
Esempio
public static event EventHandler GlobalEvent; // Ops!
// Le istanze perdono individualità, le iscrizioni si mescolano insieme
Come evitarlo?
Rendi uno evento statico solo se serve a livello globale (per esempio per un logging globale). Altrimenti mantieni l'incapsulamento a livello di istanza.
11. Problemi con capture di variabili in lambda
Essenza dell'errore
Le lambda catturano variabili per riferimento. Nei loop questo spesso porta al "valore finale".
Esempio
for (int i = 0; i < 5; i++)
{
button.Click += (s, e) => Console.WriteLine(i);
}
// Tutti gli handler stamperanno "5"
Come fare correttamente?
for (int i = 0; i < 5; i++)
{
int copy = i; // Copia locale
button.Click += (s, e) => Console.WriteLine(copy);
}
12. Mescolare weak e strong reference: Advanced “Weak Events”
In grandi applicazioni (per esempio WPF) si usa il meccanismo dei "weak events", dove il publisher mantiene una weak reference al subscriber per non impedire il garbage collection. I weak events aiutano contro i leak, ma il subscriber può essere raccolto e non ricevere l'evento.
Dettagli: Weak Event Patterns (MSDN)
13. Mancanza di standard per naming e firme degli eventi
Essenza dell'errore
Rispetta le firme e i nomi standard: gli eventi dovrebbero essere al passato (Changed, Closed, Completed), gli argomenti dovrebbero ereditare da EventArgs.
Esempio "sbagliato":
public delegate void SomethingHappens(int what);
// ...
public event SomethingHappens Something;
Esempio "corretto":
public event EventHandler<EventArgs> SomethingHappened;
Per i tuoi eventi usa quasi sempre EventHandler o EventHandler<T>. I colleghi (e il tuo futuro te) te ne saranno grati.
GO TO FULL VERSION