CodeGym /Cursos /C# SELF /Errores típicos con delegates y events

Errores típicos con delegates y events

C# SELF
Nivel 54 , Lección 0
Disponible

1. Introducción

Trabajar con delegates y events en C# es cómodo y agradable — el lenguaje hace mucho por ti. Pero detrás de esa máscara hay bastantes escollos: fugas de memoria invisibles, bugs raros por suscripciones dobles, desincronización de handlers e incluso excepciones inesperadas durante el broadcast. Los delegates y events son potentes, pero requieren trabajar con cuidado el ciclo de vida de los objetos, entender threading y saber cómo funcionan exactamente las invocaciones y desuscripciones. Si tus events funcionan "casi siempre", pero a veces no disparan o causan errores extraños, — no estás solo. Vamos a ver dónde suelen fallar incluso los desarrolladores experimentados y cómo evitarlo.

Tabla de errores principales

Error Consecuencia Cómo evitar
Suscripción doble El handler se ejecuta varias veces Vigila las suscripciones, quita antes de añadir
No desuscribirse (fuga de memoria) Los subscribers se quedan en memoria, “zombies” Siempre desuscribirse, usar IDisposable
Excepción en un handler Los demás handlers no se ejecutan try/catch en handlers o iterar manualmente
Modificar suscriptores durante el evento Saltos o duplicados en las llamadas Iterar sobre una copia de handlers (GetInvocationList())
Llamar evento cuando MyEvent == null NullReferenceException Comprobar null, usar ?.Invoke
Firma del handler no coincide Error de compilación Revisar la firma
Llamar evento desde fuera Error de compilación Invocar solo mediante OnEventName
Eventos estáticos mal colocados Suscripciones mezcladas No hacer static sin necesidad
Problemas de closure con lambdas Valores inesperados Hacer copia de la variable

2. Suscripción múltiple y llamadas múltiples

Esencia del error

Si suscribes varias veces el mismo handler a un mismo event, cada += añade tu método a la lista del delegate. Como resultado, el handler se ejecutará tantas veces como fue añadido.

Cómo se manifiesta?

Imagina que tienes un botón y un handler de click:

Button btn = new Button();
btn.Click += OnButtonClick; // Nos suscribimos
btn.Click += OnButtonClick; // ¡Y suscripción repetida!

Ahora al pulsar el botón el método OnButtonClick se llamará dos veces. Si dentro actualizas un contador o escribes en un log, verás resultados duplicados.

Cómo arreglarlo?

Normalmente la suscripción repetida ocurre por la estructura del código — por ejemplo, si el += está en un método que se llama varias veces (en distintos ciclos de vida de la forma).

  • Controla dónde ocurre la suscripción.
  • No pongas += en fases del ciclo de vida que pueden repetirse.
  • A veces es útil hacer una suscripción “única” — antes de añadir, quitar el handler:
myEvent -= MyHandler; // Por si acaso quitamos
myEvent += MyHandler; // Luego suscribimos

Esto es seguro: si el handler no estaba, el -= no hace nada.

3. Subscriber zombi

Esencia del error

Si un subscriber se suscribe a un publisher de larga vida y no se desuscribe, el garbage collector no podrá recolectarlo: el publisher todavía mantiene la referencia al delegate del handler, y por tanto al objeto subscriber. Resultado: fuga de memoria.

Ejemplo típico

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; // ¡No olvides desuscribirte!
    }
}

Si olvidas llamar a Dispose() o no lo invocas, incluso si eliminas todas las referencias a TemporaryPopup, el objeto no será recolectado — la ventana sigue referenciando su handler.

Cómo evitarlo?

  • Implementa IDisposable en subscribers cuando su ciclo de vida sea más corto que el del publisher.
  • Usa el patrón using o llama explícitamente a Dispose():
using (var popup = new TemporaryPopup(mainWindow))
{
    // ...
} // Aquí Dispose se llamará automáticamente

En apps GUI — desuscríbete en el cierre de la ventana/forma (por ejemplo, en handlers de cierre o en el Dispose() de la forma).

4. Manejo de excepciones dentro de handlers

Esencia del error

Cuando un event invoca decenas de handlers y uno de ellos lanza una excepción, los demás no se ejecutarán.

Demostración

public event EventHandler MyEvent;

public void Raise()
{
    MyEvent?.Invoke(this, EventArgs.Empty);
}

Si uno de los métodos suscritos a MyEvent lanza una excepción, los demás no serán llamados — la cadena se rompe.

Cómo lidiar con estos errores?

  • En los handlers maneja excepciones localmente (try/catch) o propágalas conscientemente.
  • En escenarios complejos — itera manualmente por la lista de suscriptores y aísla errores de cada uno:
var handlers = MyEvent?.GetInvocationList();
foreach (var handler in handlers)
{
    try
    {
        handler.DynamicInvoke(this, EventArgs.Empty);
    }
    catch (Exception ex)
    {
        // Logging, recuperación de emergencia
    }
}

5. Cambio de la lista de suscriptores durante el broadcast

Esencia del error

Si un handler dentro del evento se desuscribe a sí mismo o a otros, eso puede distorsionar el orden de llamadas: algunos handlers se saltarán o se llamarán de nuevo.

Cómo evitarlo?

  • No modifiques las suscripciones desde handlers.
  • Si necesitas hacerlo — itera sobre una copia de la lista de delegates:
var handlers = MyEvent?.GetInvocationList();
foreach (EventHandler handler in handlers)
{
    handler(this, EventArgs.Empty);
}

6. Confusión con null del delegate (sin suscriptores)

Esencia del error

Si nadie está suscrito al event, su delegate vale null. Invocarlo sin comprobar provocará NullReferenceException.

Ejemplo (mal)

public event EventHandler MyEvent;

public void Raise()
{
    MyEvent(this, EventArgs.Empty); // Si no hay suscriptores — excepción!
}

Cómo hacerlo bien?

  • Usa la llamada segura: MyEvent?.Invoke(this, EventArgs.Empty).
  • O aplica la técnica clásica threadsafe: copia el delegate a una variable local y llámala.

7. Mezcla de delegates/events con firmas distintas

Esencia del error

Los delegates son fuertemente tipados. La firma del método-handler debe coincidir con la del delegate del event — si no, fallo en compilación.

Ejemplo

public event EventHandler<string> TextChanged;

void WrongHandler(object sender, int number) { /* ... */ }

TextChanged += WrongHandler; // ¡Error de compilación!

Usa los delegates estándar EventHandler y EventHandler<T>, y asegúrate de que las firmas coincidan exactamente.

8. Intentar invocar un event fuera del publisher

Esencia del error

event es un delegado encapsulado: el código externo no puede invocarlo directamente (solo puede añadir/quitar handlers).

Ejemplo

public class MyPublisher
{
    public event EventHandler SomethingHappened;
}

var publisher = new MyPublisher();
publisher.SomethingHappened?.Invoke(publisher, EventArgs.Empty); // ¡Error!

Cómo hacerlo bien?

La invocación la hace un método protegido/público del publisher — normalmente OnEventName. Desde fuera — solo +=/-=.

9. Errores con los accessors add y remove

Esencia del error

Los accessors custom permiten controlar la suscripción, pero es fácil romper la orden correcta de llamadas o la seguridad contra hilos.

Ejemplo

public event EventHandler MyEvent
{
    add { /* ... */ }
    remove { /* ... */ }
}

Si no estás seguro — usa la implementación estándar de events. Si haces custom, ten la doc a mano y piensa en sincronización.

10. Acceso a events desde contextos static y non-static

Esencia del error

Es fácil declarar por accidente un event static cuando debería pertenecer a la instancia. Entonces todos los objetos comparten la misma lista de suscriptores.

Ejemplo

public static event EventHandler GlobalEvent; // Ups!
// Las instancias pierden su individualidad, las suscripciones se mezclan

Cómo evitar?

Haz statics solo cuando necesites algo realmente global (por ejemplo, un logging global). En el resto de casos — encapsula a nivel de instancia.

11. Problemas con captura de variables en lambdas

Esencia del error

Las lambdas capturan variables por referencia. En bucles esto suele llevar al "último valor".

Ejemplo

for (int i = 0; i < 5; i++)
{
    button.Click += (s, e) => Console.WriteLine(i);
}
// Todos los handlers imprimirán "5"

Cómo hacerlo bien?

for (int i = 0; i < 5; i++)
{
    int copy = i; // Copia local
    button.Click += (s, e) => Console.WriteLine(copy);
}

12. Mezcla de weak y strong references: Advanced “Weak Events”

En aplicaciones grandes (por ejemplo, WPF) se usa el mecanismo de “weak events”, donde el publisher guarda una referencia débil al subscriber para no impedir el garbage collection. Los weak events ayudan contra fugas, pero el subscriber puede ser recolectado y no recibir el evento.

Detalles: Weak Event Patterns (MSDN)

13. Falta de estándar en naming y firmas de events

Esencia del error

Sigue las firmas y nombres estándar: los events en pasado (Changed, Closed, Completed), los argumentos heredan de EventArgs.

Ejemplo “mal”:

public delegate void SomethingHappens(int what);
// ...
public event SomethingHappens Something;

Ejemplo “bien”:

public event EventHandler<EventArgs> SomethingHappened;

Para tus propios events casi siempre usa EventHandler o EventHandler<T>. Tus colegas (y tu yo futuro) te lo agradecerán.

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