You are on page 1of 9

Lo que todo

desarrollador
debería saber
sobre eventos en
.NET
Rodrigo Corral
LO QUE TODO DESARROLLADOR DEBERÍA SABER SOBRE EVENTOS EN
.NET
Nivel: Intermedio
por Rodrigo Corral

La manera que las clases tienen de alertar a otras clases en los lenguajes orientados a objetos
modernos es lanzar eventos. Una clase que no expone eventos hace mucho más ardua la tarea
de los desarrolladores que la consumen a la hora de detectar cambios en su estado. Una clase
sin eventos es una clase incomunicada.

En un sentido amplio se podría decir que toda clase que diseñemos y que mantenga un estado
debería tener eventos. Si una clase mantiene estado es evidente que ese estado va a cambiar a
lo largo del ciclo de vida de los objetos que instanciemos. Es evidente también que si no
exponemos eventos, quien quiera enterarse de los cambios en el estado no tendrá más opción
que preguntar activamente a nuestro objeto, en lugar de esperar plácidamente a recibir
notificaciones. Nada nuevo bajo el sol, el viejo conocido patrón Observer.

Los eventos son imprescindibles y en .Net son ciudadanos de primera categoría (no ocurre así
en otros lenguajes como C++). Aun así, no es extraño ver malas implementaciones de eventos.
Es un tema que muchos desarrolladores creen conocer bien y sin embargo se ven a menudo
errores relacionados con la mala implementación de este concepto. Y es que implementar
bien un evento tiene más arte del que podría parecer y se pueden cometer más errores de
los que uno puede pensar. Éstos varían en gravedad desde simplemente complicar la vida a las
clases que deriven de la nuestra que expone eventos, hasta introducir condiciones de carrera
difíciles de diagnosticar, pasando por simples incorrecciones de estilo. Lo peor de caso es que,
por la propia naturaleza de estos errores, FxCop no es capaz de avisarnos de ellos.

Un evento simple
Vayamos al grano y veamos la implementación más simple posible de un evento en .Net.

Los que hemos vivido los tiempos de .Net 1.0 y 1.1 sabemos que la palabra clave event solo es
“azúcar sintáctico” que provoca que el compilador emita por nosotros un delegado y en los
tiempos de 1.x nosotros mismos teníamos que declarar el tipo de delegado a mano. Se trata de
un evento que ni siquiera implementa su propia clase de argumento para el evento. Sería algo
como se muestra a continuación:

class Publisher
{
public event EventHandler SampleEvent;

public void FunctionThatProducesTheEvent()


{
SampleEvent(this, EventArgs.Empty);
}
}
¿Cuántos errores puede ver en esta implementación? Uno, dos, tres… si no les ves sigue
leyendo.

¿Qué pasa si no hay nadie escuchando?


Generalmente, en una conversación, para que se produzca una comunicación correcta debe
haber una parte emitiendo y otra recibiendo. En .Net una clase que quiera escuchar los
eventos de otra simplemente tiene que subscribirse a ellos:
class Subscriber
{
private Publisher _publiser = new Publisher();

public Subscriber()
{
_publiser.SampleEvent +=
new EventHandler(_publiser_SampleEvent);
}

void _publiser_SampleEvent(object sender, EventArgs e)


{
Console.Write("Habemus evento");
}
}

Como ya hemos comentado lo eventos se implementan, simplificando el asunto, como


delegados. Simplificando otro poco podemos decir que los delegados son la versión orientada
a objetos de los punteros a función. Básicamente la clase que declara el evento es declarando
un tipo de puntero a función y un lugar en el que almacenar ese puntero. La clase que se
subscribe proporciona la función que realmente se ejecutará mediante un puntero a la función
durante la subscripción. Cuando la clase que lanza el evento hace la llamada al mismo
(SampleEvent en el ejemplo), simplificando, está llamando a una función a través de un
puntero. Por pura lógica si nadie se ha subscrito al evento dicho puntero será nulo, por lo
tanto invalido, y en consecuencia recibiremos una fea excepción de tipo
System.NullReferenceException.

La moraleja: siempre debemos comprobar que alguien se ha subscrito a nuestro evento


antes de lanzarlo. Más simple no puede ser:
class Publisher
{
public event EventHandler SampleEvent;

public void FunctionThatProducesTheEvent()


{
//Comprobar que tenemos subcriptores
if (SampleEvent != null)
SampleEvent(this, EventArgs.Empty);
}
}
¿Qué pasa si hay varios hilos?
El código anterior es más correcto. Pero aún tiene un error, además uno bastante sutil, que
solo se manifestaría en situaciones en las que haya varios hilos de ejecución. Es una típica
condición de carrera (o race condition). Estamos comprobando que tenemos alguien subscrito,
en la siguiente instrucción de código estamos lanzando el evento. ¿Qué impide que la clase
que estaba subscrita se haya de-suscrito entre medias? Nada. La moraleja: debemos asegurar
que si alguien estaba escuchando, siga escuchando cuando nosotros digamos algo.

La solución pasa por manejar nuestra propia copia local de la lista de subscriptores. La
implementación de esta solución es:
public void FunctionThatProducesTheEvent()
{
//Copia local de las subscripciones al evento para
//evitar la condición de carrera entre la comprobación de
//que hay subscriptores y el lanzamiento del evento.
//Aunque que todos se de-suscriban,
//nosotros tenemos la referencia.
EventHandler handler = SampleEvent;

//Comprobar que tenemos subcriptores


if (handler != null)
handler(this, EventArgs.Empty);
}

Aunque todos se de-suscriban nosotros seguimos teniendo una referencia. Las consecuencias
de esta solución (que es la implementación correcta si seguimos el patrón de eventos de .Net)
son varias:

1) Una clase que se haya de-suscrito de un evento, en un entorno multihilo, puede aun
así recibirlo temporalmente. Si este comportamiento no es aceptable, tendremos que
utilizar algún mecanismo de sincronización.
2) Si una clase está suscrita a un evento no podrá ser recolectada por el recolector de
basura, pues aun quedarán referencias a ella. Esta es una forma muy sutil de fugar
memoria de objetos en .Net: olvidar de-suscribir la clase de los eventos a los que está
subscrita. Si esto no fuese así, no habría manera de garantizar que siempre hay alguien
escuchando. Por lo tanto: si no de-suscribes tus objetos de los eventos a los que
estén suscritos, pertenecientes a objetos con mayor tiempo de vida, el recolector de
basura no puede llevárselos al otro mundo. Un patrón que funciona bastante bien es
implementar IDisposable y de-suscribirnos de todos los eventos a lo que la clase se ha
suscrito en el método Dispose.

¿Qué pasa si derivo de una clase que expone eventos?


Supongamos que derivamos una clase de la clase base que expone eventos. Todos sabemos
que el motivo para derivar una clase de otra es modificar o extender su comportamiento.
Lógicamente uno de los aspectos que nos puede interesar variar del comportamiento de una
clase es cómo se comportan sus eventos, qué ocurre cuando se lanzan, qué información
acompaña al evento, etc…
Cuando se trata de modificar el comportamiento de un método de una clase base, podemos
sobrescribir dicha función, el problema es que no podemos sobrescribir un evento. La solución
al problema es simple: lanzar todos los eventos desde una función protegida y virtual, en
lugar de directamente, de tal manera que una clase derivada pueda redefinir el
comportamiento del evento a su gusto o incluso anular su lanzamiento sobrescribiéndola. La
moraleja: debemos dar a las clases derivadas la oportunidad de modificar el comportamiento
del lanzamiento del evento.

Además, con esa función damos a las clases derivadas la posibilidad de lanzar el evento si lo
necesitan, simplemente invocando a la función que lanza el evento.

Con lo comentado anteriormente la implementación de nuestro evento quedaría como sigue:


class Publisher
{
public event EventHandler SampleEvent;

public void FunctionThatProducesTheEvent()


{
//Hacer algo aquí...

//Lanzar el evento
OnSampleEvent();
}

protected virtual void OnSampleEvent()


{
//Copia local de las suscripciones al evento para
//evitar la condición de carrera entre la comprobación de
//que hay subscriptores y el lanzamiento del evento.
//Aunque que todos se de-suscriban,
//nosotros tenemos la referencia.
EventHandler handler = SampleEvent;

//Comprobar que tenemos subcriptores


if (SampleEvent != null)
SampleEvent(this, EventArgs.Empty);
}
}

De esta manera, cualquier clase que derivase de la nuestra podría modificar el


comportamiento del evento a su gusto simplemente sobrescribiendo la función
OnSampleEvent. Una convención a tener en cuenta: si nuestro evento se llama XYZ la función
virtual asociada debe llamarse OnXYX.

¿Qué pasa si además hay información asociada al evento?


La firma de un evento declarado con EventHandler es:
public delegate void EventHandler(object sender, EventArgs e);
Con esta firma podemos detectar en la función que maneja el evento cual es el objeto que
originó el evento -en el parámetro sender- e información asociada al evento -en el parámetro e
de tipo EventArgs. Si vemos la definición de la clase EventArgs, veremos que es de nula
utilidad, ya que no tiene campos que contengan información. El propósito de esta clase es
servir como clase base para nuestros propios argumentos de evento.

Supongamos que quisiésemos que cuando salte nuestro evento, quien lo reciba, reciba
además cierta información. Por ejemplo nos podría interesar saber a qué hora se produjo el
evento. En esta situación lo primero es derivar una clase de la clase EventArgs que incluya la
información que nos interesa:
class SampleEventArgs : EventArgs
{
readonly private DateTime _eventDateTime = DateTime.Now;

public DateTime EventDateTime


{
get { return _eventDateTime; }
}
}

Ahora lógicamente necesitamos cambiar la firma del delegado que manejará el evento. Para
eso, desde .Net 2.0 o superior, tenemos una implementación genérica de la clase EventHandler
que nos permite especificar el tipo de nuestro EventArgs. Es suficiente por tanto con cambiar
la declaración del evento adecuadamente y corregir los errores de compilación. Con lo que la
implementación de nuestra clase que expone eventos, quedaría definitivamente, como sigue:
class Publisher
{
public event EventHandler<SampleEventArgs> SampleEvent;

public void FunctionThatProducesTheEvent()


{
//Hacer algo aquí...

//Lanzar el evento
OnSampleEvent();
}

protected virtual void OnSampleEvent()


{
//Copia local de las subcripciones al evento para
//evitar la condición de carrera entre la comprobación de
//que hay subscriptores y el lanzamiento del evento.
//Aunque que todos se de-suscriban,
//nosotros tenemos la referencia.
EventHandler<SampleEventArgs> handler = SampleEvent;

//Comprobar que tenemos subcriptores


if (SampleEvent != null)
SampleEvent(this, new SampleEventArgs());
}
}
La moraleja: Si necesitamos transmitir información junto con el evento debemos derivar una
clase de EventArgs contenedora de la información y usar la implementación genérica de
EventHandler.

Una clase subscrita podría extraer fácilmente la información adicional asociada al evento:
class Subscriber
{
private Publisher _publiser = new Publisher();

public Subscriber()
{
_publiser.SampleEvent +=
new EventHandler<SampleEventArgs>(_publiser_SampleEvent);
}

void _publiser_SampleEvent(object sender, SampleEventArgs e)


{
Console.Write("Se lanzo el evento a las {0}",
e.EventDateTime);
}
}

Resumen de reglas para eventos


 No es extraño ver malas implementaciones de eventos.
 Siempre debemos comprobar que alguien se ha suscrito a nuestro evento antes de
lanzarlo y esta comprobación debe ser ‘thread safe’.
 Una clase que se haya de-suscrito de un evento, en un entorno multihilo, puede aun
así recibirlo momentáneamente.
 Si nuestro evento se llama XYZ la función virtual asociada debe llamarse OnXYX.
 Si no de-suscribes tus objetos de los eventos a los que estén suscritos, de objetos con
mayor tiempo de vida, el recolector de basura no puede llevárselos al otro mundo.
 Debemos dar a las clases derivadas la oportunidad de modificar el comportamiento del
lanzamiento de los eventos y de lanzarlos si lo necesitan.
 Si necesitamos transmitir información junto con el evento debemos derivar una clase
de EventArgs contenedora de la información y usar la implementación genérica de
EventHandler.

A que no pensabas que los eventos daban para tanto… ;-)


Acerca del autor
Rodrigo Corral es MVP de ASP.NET y tutor de campusMVP. Trabaja como analista en Sisteplant, una
empresa líder en el sector de las aplicaciones de gestión industrial. Anteriormente ha trabajado como
responsable de desarrollo y analista-programador en Panda Software. Asimismo es formador en
diversas materias tales como patrones de software, gestión de la configuración y Herramientas de
diseño, etc.. Rodrigo es también Instructor Certificado de Microsoft (MCT). Es consultor y formador en
Plain Concepts.

Acerca de campusMVP
CampusMVP te ofrece la mejor formación en tecnología Microsoft a través de nuestros cursos online y
nuestros libros especializados, impartidos y escritos por conocidos MVP de Microsoft. Visita nuestra
página y prueba nuestros cursos y libros gratuitamente. www-campusmvp.com

Reconocimiento - NoComercial - CompartirIgual (by-nc-sa):


No se permite un uso comercial de este documento ni de las posibles obras derivadas, la
distribución de las cuales se debe hacer con una licencia igual a la que regula esta obra original. Se
debe citar la fuente.