Emitiendo para toda la galaxia, ¿hay alguien ahí?: Lo que todo desarrollador debe saber sobre los eventos en .Net

La manera que las clases tiene 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 un clase incomunicada, que dirían los O’funk’illo.

En en 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 eventos. 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 que varían en gravedad desde complicar la vida a las clases que deriven de nuestra clase que expone eventos hasta introducir condiciones de carrera difíciles de diagnosticar, pasando por simples incorrecciones de estilo. Lo peor de caso, es que al contrario que en otras ocasiones, por la propia naturaleza de estos errores, FxCop no es capaz de avisarnos de ellos.

Vamos al grano. Veamos la implementación más simple posible de un evento en .Net 2.0, 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. En los tiempos de 1.0 y 1.1 nosotros teníamos que declarar el tipo de delegado ‘a manopla’. Se trata de un evento que ni siquiera implementa su propia clase de argumento del evento. Sería algo como sigue:

    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. Evidentemente esto no aplica si se trata de un grupo de mujeres. Pueden estar todas emitiendo, ninguna escuchando y aun así todas enterarse… misterios de la naturaleza.

En .Net un 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 la 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á, proporcionando el puntero a la función mediante la subscripción. Cuando la clase que lanza el evento hace la llamada al evento (SampleEvent en el ejemplo), simplificando, esta llamando a una función a través de un puntero. Es de lógica que si nadie se a subscrito 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 OnEvent;

 

        public void FunctionThatProducesTheEvent()

        {

            //Comprobar que tenemos subcriptores

            if (SampleEvent != null)

                SampleEvent(this, EventArgs.Empty);

        }

    }

¿Que pasa si hay varios hilos?

El código anterior es más correcto. Pero aun tiene un error, además un 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. 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 desuscrito 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 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 desuscriban, nosotros tenemos la referencia.

            EventHandler handler = SampleEvent;

 

            //Comprobar que tenemos subcriptores

            if (handler != null)

                handler(this, EventArgs.Empty);

        }

Aunque todos se desuscriban 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 desuscrito 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á subscrito 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 objetos en .Net: olvidar desuscribir 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. La moraleja: si no desuscribes 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. Un patrón que funciona bastante bien es implementar IDisposable y desuscribirnos de todos los eventos a lo que la clase se a subscrito en el método Dispose.

¿Qué pasa si derivo de un 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 no puede interesar modificar del comportamiento de una clase es como se comportan sus eventos, que ocurre cuando se lanzan, que información acompaña al evento, etc… Cuando se trata de una modificar el comportamiento de un método de una clase base, podemos sobreescribir dicha función, el problema es que no podemos sobreescribir 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 reescribiendo esta función. 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 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 desuscriban, 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 sobreescribiendo la función OnSampleEvent. La moraleja: si nuestro evento se llama XYZ la función virtual asociada debe llamarse OnXYX.

¿Qué pasa si además tengo algo que contar asociado 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 origino 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 nuestras 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 que 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 tenemos una implementación genérica de la clase EventHandler que nos permite especificar el tipo de nuestro EventArgs. Basta por tanto 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 desuscriban, 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);

        }

    }

Corolario:

  • No es extraño ver malas implementaciones de eventos.
  • Siempre debemos comprobar que alguien se ha subscrito a nuestro evento antes de lanzarlo y esta comprobación debe ser ‘thread safe’.
  • Una clase que se haya desuscrito 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 desuscribes 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 pensabais que los eventos daban para tanto… ;).

He leído: The old new thing de Raymon Chen

Una de las liturgias de mi familia y mía en particular es almorzar donde mi abuela Basi. La liturgia es muy simple, siempre que llego a mi pueblo, Belorado, a eso de media mañana, pongo el culo en una de las sillas de la cocina de mi abuela y degusto alguno de los manjares que prepara: unos huevos fritos o en salsa, una morcillita asada, un trocito de queso, chorizo, pancetita, asadurilla, bacalao… vamos el típico almuerzo castellano. Supongo que muchos ya estaréis salivando pero el tema no es la gastronomía. Ahora me explico, paciencia.

Mientras mi abuela concina y nosotros almorzamos, mi abuela no calla. No para de hablar ni un segundo. Generalmente sobre los pequeños asuntos familiares. Pero, en las grandes ocasiones en las que tengo el placer de ser el único comensal mi abuela me cuenta un motón de historias. Historias sobre mi pueblo, sobre su gente, sobre lo que ha pasado esa semana, sobre lo que paso en la guerra, sobre mi abuelo en Alemania, sobre la vida en el campo, sobre la infancia de mi padre y mi tío, sobre cuando vivían en San Nicolas … Historias inconexas, historias que a veces no tiene sentido para mi, , que divierten o que aburren soberamente (las menos de las ocasiones) y a veces autenticas perlas de sabiduría, historias que enseñan un motón casi siempre, sobre la vida, sobre mi pueblo, sobre mi familia. Muchas veces no conozco a la mayoría de los personajes: Si hombre, la hija de tal, que fue a vivir no se donde, que es prima de ese que es padre de aquella que es de tu cuadrilla que salía con el primo de tu amigo aquel de Galdakano, dice mi abuela y yo digo, si si ya se, no por que sepa sino por no liar más la madeja. Seguro que los que tenéis abuela sabéis lo que digo.

Pues bien, de eso va este libro de Raymon Chen, de historias sobre su pueblo, que da la casualidad que es Windows y su API, en el que lleva viviendo desde la versión 1.0. Las historias que recoge, son como las de mi abuela: entretenidas, aburridas, unas se entienden y otras no, todas tiene algo en común, aunque muchas veces sea difícil ver la línea argumental y en otras no exista.

Resumiendo, y aquí es donde vamos al grano del asunto, que Raymon Chen se ha puesto a contar todas sus historias de abuelo cebolleta. Se trata de un abuelo que lo sabe todo todo todo sobre Windows, su desarrollo, su historia y su interioridades.

El libro es de un valor inapreciable para entender la dificultad que entraña el tomar decisiones que pueden afectar a millones de usuarios y desarrolladores. Además explica un montón de interioridades de Windows y justifica por que muchas cosas son como son en nuestro sistema operativo favorito. Hay artículos sobre como se pintan las ventanas, como se manejan los mensajes (¡por fin lo he entendido! solo por esto ha merecido la pena leer el libro), chascarrillos sobre el equipo de desarrollo de Windows, interioridades de COM, trucos de depuración, etc… Hay artículos muy técnicos y artículos que podría entender cualquiera. Eso sí, si has peleado con el API de Windows desde C, te vas a sentir como en tu pueblo, sino el libro puede ser un poco árido, es la única pega que tiene. Hay que tener claro que el pueblo de Chen es Windows, su shell, el API, COM y C, nada de .Net. Vamos Windows de verdad de la buena.

Un libro bastante recomendable y que merece bastante la pena comprar. Si bien tiene pasajes difíciles de digerir (si si abuelo, ya se de que me hablas, sigue hacia adelante no te enrolles), es lo suficientemente ameno como para recomendar su lectura a cualquier desarrollador que disfrute desarrollando en Windows.

Por último, si queréis saber de antemano el tipo de historias que encontraréis, podéis revisar el blog de Raymon Chen, que lleva el mismo título que el libro.