CodeGym /Corsi /C# SELF /Errori tipici con delegate ed eventi

Errori tipici con delegate ed eventi

C# SELF
Livello 54 , Lezione 0
Disponibile

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.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION