Página principal
Artículos y trucos
Catálogo de productos
Ejemplos y descargas
Mis libros
Cursos de formación
Investigación y desarrollo
Libros recomendados
Mis páginas favoritas
Acerca del autor
 
En colaboración con Amazon
 
Intuitive Sight

C# versus Java: eventos

No puedes enseñarle a un hombre lo que él cree que ya conoce.
Epícteto
Cuando un tonto tira por un camino, o se acaba el camino, o se acaba el tonto...
Refrán popular

Nunca he ocultado mi mala relación con Java. Este es un lenguaje que sacrifica libertad, mucha libertad, por un poco de seguridad aparente. Reconozco, por lo tanto, que una parte importante de mi actitud tiene un origen subjetivo. Por este motivo, de vez en cuando hago examen de conciencia, intentando ser lo más objetivo posible al evaluar las peculiaridades de este lenguaje. Esta vez le ha tocado su turno al sistema de eventos de Java (ver también la comparativa sobre genericidad y otras características de estos lenguajes).

  1. Funciones de respuesta y tipos de interfaz
  2. Eventos en Java
  3. Clases anidadas en Java
  4. ¿Por que hace falta una sintaxis especial para eventos?
  5. Los delegados de C#
  6. Eventos, hablando con propiedad
  7. Vidas y hazañas de eventos y delegados
  8. Métodos anónimos versus clases anónimas

FUNCIONES DE RESPUESTA Y TIPOS DE INTERFAZ

Suponga que somos los diseñadores de un nuevo lenguaje orientado a objetos. Estamos en pleno siglo XXI y conocemos las experiencias anteriores de lenguajes como Delphi, Java, Visual Basic y C#. Sabemos que necesitamos alguna técnica para implementar "eventos": tendríamos que argumentar ahora por qué es necesario trabajar con eventos, pero para no hacer demasiado largo este artículo, vamos a asumir que conocemos la argumentación.

¿Cómo implementaremos estos eventos? Veamos primero lo que hacen otros lenguajes: por ejemplo, Delphi. Delphi no fue el primer lenguaje en definir eventos, pero su solución fue la primera solución elegante (hasta donde llegan mis conocimientos). En Delphi, un evento se representa mediante un puntero especial de ocho bytes, formado por dos punteros tradicionales de cuatro bytes: uno apunta a un objeto o instancia, y el otro a la dirección del método dentro del segmento de código. Claro, puede que aquí suenen las alarmas: "¿punteros? ¡los punteros son malvados!". ¿No habrá otra forma de implementar eventos?

Pues sí, resulta que existe otra vía: usemos tipos de interfaz, una técnica que ha venido empleándose desde hace mucho tiempo. Teóricamente, un puntero a interfaz es un puntero a una caja negra que implementa determinados métodos. En la práctica, la gran mayoría de los lenguajes implementan esa "caja negra" como un vector de punteros a métodos (punteros de cuatro bytes esta vez, porque el puntero al objeto físico está representado implícitamente). Como comprenderá, un tipo de interfaz es una forma más elegante de representar, no un puntero a método al estilo Delphi, sino varios de estos punteros a la vez.


En C++ no existen los tipos de interfaz, y deben simularse mediante clases abstractas, en cuyo interior sólo existen métodos virtuales puros (nada de campos, por ejemplo). Si nunca ha trabajado con tipos de interfaz, puede imaginárselos, en una primera aproximación, como clases abstractas.

Veamos un ejemplo, con la sintaxis de C#, para simplificar (en realidad, C# utiliza un sistema de eventos completamente diferente, como veremos más adelante). Suponga que tenemos que crear una clase para un botón; el botón, cuando es pulsado, debe "avisar" a quien o quienes estén interesados en este suceso. Primero tendríamos que definir un tipo de interfaz que deberían implementar los objetos interesados en recibir avisos por parte de un botón:

public interface IOyenteClic    // Lo he bautizado en castellano
{
   void Clic(object emisor);    // He simplificado los parámetros
}

Supongamos ahora que la clase de mi botón se llama MiBoton. Está implementada como un control (no importa ahora la plataforma o sistema operativo), que cuando se da cuenta que ha sido pulsado, ejecuta el siguiente método:

public class MiBoton
{
   // ...
   // Un puntero a un objeto que implementa IOyenteClic
   public IOyenteClic oyente = null;

   // El método que ejecuta el botón al ser pulsado
   protected virtual void BotonPulsado()
   {
      if (oyente != null)
         oyente.Clic(this);
   }
}

Si algún objeto, digamos que un formulario, estuviese interesado en recibir una notificación proveniente del botón cada vez que sea pulsado, sólo tendría que asignarse a sí mismo en la variable oyente del botón. Por ejemplo, esto podría ser parte de la inicialización de un formulario, que inmediatamente después de crear y configurar un botón, informase al éste su interés en recibir notificaciones:

public class Form1: Form, IOyenteClic
// ¡¡¡Observe que esta clase implementa IOyenteClic!!!
{
   public Form1()
   {
      // ...
      MiBoton b = new MiBoton();
      b.oyente = this;  // Nos registramos para recibir el evento
      // ...
   }
   // La implementación del método Clic de IOyenteClic
   void IOyenteClic.Clic(object emisor)
   {
      MessageBox.Show("Botón pulsado");
   }
   // ...
}

Observe, en primer lugar, que para que un objeto pueda recibir notificaciones del botón, debe primero implementar la interfaz definida por éste: IOyenteClic. Y luego, debe asignarse a sí mismo en la variable oyente, definida como pública por el botón.


Este ejemplo inicial está muy simplificado. Normalmente, si llegásemos a aplicar una técnica tan tonta, tendríamos una propiedad Oyente, en vez de un simple campo. En C#, además, los eventos permiten avisar a más de un objeto, mientras que en este ejemplo sólo podemos avisar a uno.

Aunque se trata de una técnica muy básica, funciona. De hecho, la implementación de eventos en COM es muy parecida a este sistema, excepto que el botón contendría en realidad una lista de punteros de interfaz, para poder avisar a más de un receptor del evento.

Sin embargo, aunque el uso de tipos de interfaz para lanzar eventos es más que suficiente para COM, su uso en lenguajes de programación de propósito general provocaría una serie de problemas. Vamos a plantearnos un escenario muy común: hay dos botones sobre un formulario, y estamos interesados en los eventos disparados por ambos botones. Supongamos también que la respuesta a los eventos de cada botón son muy diferentes. ¿Podríamos implementar dos veces la interfaz IOyenteClic, dentro de la misma clase? Está claro que no. Todos los botones tendrían que compartir el mismo manejador del evento dentro del formulario. Y hay más: los diseñadores de componentes tendrían que poner sumo cuidado en no definir tipos de interfaz para eventos con el mismo nombre en clases diferentes.

EVENTOS EN JAVA

Pongamos ahora toda nuestra atención en Java. Los diseñadores de Java eran personajes que odiaban los punteros, y nunca hubiesen aceptado implementar los eventos al estilo Delphi. De hecho, ni siquiera tuvieron en cuenta el soporte para eventos cuando crearon Java. Este lenguaje no es un buen lenguaje para trabajar con componentes (lo explicaré en otro momento), y uno de los signos que lo demuestran es la ausencia de soporte específico para eventos y propiedades.

Por lo tanto, el mecanismo inicial de disparo y manejo de eventos en Java se basó en el uso de interfaces. Por ejemplo, para interceptar el clic de un botón, se hacía algo parecido a esto:

// Esto es Java, no C#
public class MainForm
   extends Frame implements ActionListener
{
   public MainForm()
   {
      // ...
      Button b = new Button("Click me");
      b.addActionListener(this);
      // ...
   }
   public void actionPerformed(ActionEvent ev)
   {
      // Aquí hacemos "algo"
   }
   // ...
}

Por supuesto, se trata de la misma técnica mostrada antes; la única diferencia es que el botón puede enviar notificaciones a más de un objeto, como sugiere el nombre del método con el que registramos el "oyente": addActionListener. Esto significa que seguimos teniendo el problema antes explicado: ¿qué sucederá cuando tengamos más de un botón? Si no hacemos algo, todas las notificaciones irán a parar al mismo método, y estaremos obligados a identificar el objeto emisor mediante métodos del parámetro del evento (¡recuerde que en Java no existen propiedades!):

public void actionPerformed(ActionEvent ev)
{
   if (ev.getSource() == button1)
      Boton1Pulsado();
   else if (ev.getSource() == button2)
      Boton2Pulsado();
   // ...
}

Está claro que, en una aplicación grande, esta técnica conduciría al caos. La solución ideada por los "javalíes" (el animal es "jabalí"; "javalí" es el programador ciego de adoración por Java) fue utilizar objetos de escucha intermediarios, declarando nuevas clases:

// Una clase auxiliar
public class Intermediario implements ActionListener
{
   public void actionPerformed(ActionEvent ev)
   {
      // Aquí hacemos algo
   }
}

public class MainForm
   extends Frame implements ActionListener
{
   Button b1, b2;

   public MainForm()
   {
      // ...
      b1 = new Button("Click me");
      // Creamos el primer intermediario
      b1.addActionListener(new Intermediario());
      // ...
      b2 = new Button("Hit me");
      // Creamos el segundo intermediario
      b2.addActionListener(new Intermediario());
      // ...
   }
   // ...
}

La situación es ahora la siguiente:


Cuando se pulsa alguno de los botones, el aviso se envía a uno de los objetos de tipo Intermediario que hemos creado. Hablando en plata, lo que ocurre es que se ejecuta un método de uno de estos objetos. Resultado: un nuevo objeto por cada evento interceptado, pero no tendremos el problema de compartir un mismo método entre montones de eventos. Ahora bien, ¿qué puede hacer un "intermediario" una vez que recibe un evento? Pues muy poca cosa, mientras no tenga acceso al objeto donde hemos situado los botones.


En realidad, tenemos otro problema más grave. Hemos creado una sola clase para dar tratamiento a dos eventos diferentes, cuando en realidad necesitaremos dos clases: una clase para cada evento.

Por lo tanto, lo más frecuente será que a la clase Intermediario se le facilite un puntero a la instancia donde realmente está teniendo lugar el espectáculo:

public class Intermediario implements ActionListener
{
   MainForm mainForm;

   public Intermediario(MainForm mf): mainForm(mf) {}

   public void actionPerformed(ActionEvent ev)
   {
      // Aquí hacemos algo sobre "mainForm"
   }
}

Para asociar un receptor al evento del primer botón haríamos esto:

      b1 = new Button("Click me");
      // Creamos el primer intermediario
      b1.addActionListener(new Intermediario(this));

Si sólo mostramos un botón, para simplificar, la nueva situación se parecerá a la del siguiente diagrama:


¿Le parece complicado? Espere entonces a ver la siguiente "simplificación" que introdujeron estos "genios"...

CLASES ANIDADAS EN JAVA

El problema principal de esta técnica es la proliferación de clases que provoca. Para cada evento manejado, hay que crear una clase, y una instancia de esa clase; incluso es más absurdo: una sola instancia de la clase intermediaria. Una de las consecuencias de esta epidemia es que habría que inventarse nombres diferentes para cada clase intermediaria, para evitar conflictos. Además: ¡habría que escribir montones de líneas de código para atrapar un simple evento! La solución ideada por los "javalíes" fue echar mano de clases anidadas: clases que se definen dentro de otra clase, y que evitan de este modo los conflictos en el primer nivel de nombres. En realidad, Java tiene sobreabundancia de tipos de clases anidadas:

  1. Clases anidadas estáticas.
  2. Clases anidadas "normales" (¡es complicado traducir inner member classes!).
  3. Clases locales.
  4. Clases anónimas.

¡Mire cuántas complicaciones nos está causando el horror de Jaime Gansito y Guille Alegría a los punteros! Es difícil justificar la existencia de la mayoría de estos tipos de clases anidadas sin mencionar el problema del tratamiento de eventos. De esta turbamulta, nos interesan en concreto las clases anónimas, o anonymous classes: se trata de clases sin nombres que se definen como parte de una instrucción. Suena extraño, ¿verdad? Es mejor que lo explique con un ejemplo:

      b1 = new Button("Click me");
      // Creamos el primer intermediario
      b1.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent ev)
         {
            // ¡¡¡Este método tiene acceso directo...
            // ... a la instancia que lo contiene!!!
            b1.setVisible(false);
            // Recuerde que b1 es un campo de la clase exterior
         }
         });

El texto destacado en azul, y encerrado entre llaves, corresponde a toda una declaración de una clase. Antes del bloque que encierra la clase, el operador new crea una instancia de esta clase sin nombre. La clase implementa, sin que tengamos que hacerlo explícito, el tipo de interfaz ActionListener. Lo más interesante, sin embargo, es lo que destaca el comentario: la instancia creada de la clase anónima puede referirse a los campos y métodos de la clase que la encierra sin mayor dificultad. En el ejemplo anterior, estoy escondiendo el botón pulsado, algo muy sencillo, y hago referencia a la variable b1 como si se tratase de un campo definido dentro de la clase anónima. Está claro que el compilador de Java, a marcha forzada, ha añadido un parámetro "oculto" al constructor de la clase anónima para pasar una referencia a la clase que la contiene. Esa referencia se almacena en un campo privado añadido por Java a la clase anónima. Esta clase también podría utilizar variables locales del método donde se instancia. En ese caso, la técnica usada por el compilador consiste en copiar el contenido de esas variables en campos locales de la clase anónima, nuevamente gracias a manipulaciones en el constructor de la misma...

... ¿no le extraña que esté hablando del constructor de la clase anónima? En realidad, ¡no podemos declarar un constructor para una clase anónima! Los constructores en Java, al igual que en C++ y C#, se definen usando el mismo nombre de la clase... algo evidentemente imposible en el caso que estamos analizando. Se trata de una de las anomalías y peligros introducidos por este chapucero recurso. Tenga en cuenta que, aunque las clases anónimas resuelven el problema de la proliferación de nombres de clases, no resuelven la proliferación de las propias clases. El código final es bastante engorroso de escribir, leer y mantener (cuente y paree los paréntesis y llaves necesarias). Y, como demuestran las pruebas, el mecanismo resultante es bastante lento.

¿POR QUE HACE FALTA UNA SINTAXIS ESPECIAL PARA EVENTOS?

Suponga que somos los creadores de un nuevo lenguaje orientado a objetos... y que ésta vez sí sabemos lo que estamos haciendo. Sabemos, por ejemplo, que debemos soportar eventos. Antes de la aparición de Delphi y Visual Basic, para lograr que un botón mostrase un mensaje al ser pulsado, debíamos crear una clase derivada por herencia de la clase original del botón. La herencia, sin embargo, es un asunto muy serio para jugar con ella despreocupadamente. Para empezar por lo obvio: declarar e implementar una clase derivada exige más líneas de código que ... bueno, que la alternativa, que como veremos, consiste en asignar la dirección de un método en una propiedad. Aproximadamente.


¿Le parece una tontería lo del número de líneas? La productividad de un programador tiene que ver más con el número de líneas que tiene que escribir, más que con el nivel de abstracción del lenguaje. Por supuesto, los lenguajes de menor nivel de abstracción suelen necesitar muchas más instrucciones para ejecutar el mismo algoritmo que en un lenguaje suficientemente expresivo requeriría un par de líneas.

Por supuesto, hay razones más elegantes para justificar la necesidad de una construcción sintáctica especial para eventos. Por ejemplo, en los lenguajes orientados a objetos actuales, la pertenencia a una clase es una característica que va unida a la identidad de una instancia. O dicho en menos palabras: un objeto no puede cambiar su clase, una vez creado. Por lo tanto, es complicado cambiar radicalmente el comportamiento de un objeto ya creado; sí, existen trucos y patrones para ello... pero nuevamente gastan demasiadas líneas. En cambio, un buen sistema de eventos permite que un objeto deje de actuar de una manera y cambie su personalidad en un instante.


Voy a mencionar un punto a favor de Delphi.NET comparado con C#... ¡pero no se acostumbre! Se trata de los métodos de clase, que se diferencian de los métodos estáticos de C# (shared, en VB.NET). Estos métodos de clase recibe un puntero oculto, que se puede obtener mediante el identificador Self. Pero Self no apunta a una instancia de la clase, como en los métodos de instancia tradicionales, ¡sino a la propia clase! Este modelo todavía no existe en .NET, que nos obliga a utilizar reflexión para algunas de las tareas que resolvían los métodos de clase de Delphi. Además, los métodos de clase de Delphi, en combinación con otro recurso único de Delphi, los constructores virtuales, permitían la construcción polimórfica: con una sola instrucción, podíamos crear un objeto cuya clase "exacta" no se podría predecir en tiempo de ejecución. Esta funcionalidad podría implementarse mediante el patrón Factory, pero nos costaría muchas más líneas de código.

Pero éstamos pasando por alto una importantísima característica de los eventos, relacionada con la forma de usarlos. En las primeras bibliotecas de clases para el desarrollo de aplicaciones gráficas en Windows, para hacer algo en respuesta al clic de un botón era necesario redefinir un método virtual en una clase heredada. Esta es una versión en C# de lo que se hacía en aquel entonces:

class BotonQueHaceAlgo: Button {
  // ...
  public override void Click() {
    // Hacemos algo especial
    EncenderFuegosArtificiales();
    // ... y ahora viene lo "bueno"...
    base.Click();             // <---- ¿Es realmente necesario?
  }
}

Por inercia, hemos incluido una llamada al método Click original en la nueva implementación del mismo. ¿Era necesario hacerlo? Porque si la versión original era abstracta, esto provocaría un error de compilación (C#) o de ejecución (Delphi), en dependencia del lenguaje. Supongamos que sí, que esa llamada era necesaria. ¿Dónde habría que ubicarla? ¿Antes de encender los petardos o después? Demasiadas preguntas para manejar el clic de un botón.

Para resolver estas dudas, los eventos se diseñan con una regla muy simple en mente: el componente que lanza un evento debe funcionar sin errores, de manera razonable, si al programador que utiliza el componente se le olvida interceptar el evento. La lógica básica del componente debe estar programada dentro del componente, nunca debe dejarse a cargo del programador, para que éste la escriba en la respuesta a un evento. En relación con esta regla, se dice que los eventos son "contract free" (¿libres de contrato?). Redefinir un método virtual, como en el ejemplo anterior, no es contract free, y nos obliga a informar al programador lo que esperamos de él.

¿Y qué pasa con los eventos basados en interfaces, al estilo Java? Obsérvelo usted mismo. Cuando un componente expone un evento en Java, publica un método para registrar oyentes. Simplifiquemos: supongamos que se trata de un evento de unidifusión, al estilo Delphi, y que en vez de utilizar un método de registro, vamos a exponer directamente el puntero de interfaz subyacente:

class MiBoton {
  // ...
  public IOyenteClick eventoBoton;
}

La variable eventoBoton apuntará en tiempo de ejecución a un objeto cuya clase debe implementar IOyenteClic. ¿Son contract-free las llamadas a los métodos de esa interfaz? ¡No se puede deducir sólo con el texto de la clase! Antes, le he advertido que se trata de un evento. Un programador de Java tendría que incluir un comentario al respecto, de manera que sólo los humanos sabremos que la implementación a la que hace referencia eventoBoton debe ser contract-free... solamente los humanos, no el compilador. ¿No sería todo más sencillo si tuviésemos una marca semántica para un recurso tan usado?

LOS DELEGADOS DE C#

Veamos ahora cómo resuelve C# estos problemas. Hay dos construcciones en este lenguaje que debemos conocer y distinguir:

  • Delegados: Un delegado, o delegate, es un tipo de datos; en realidad, un tipo de clase muy especializado. Corresponde aproximadamente a los punteros a métodos de Delphi, aunque veremos que hay grandes diferencias.
  • Eventos: Un evento no es un tipo de datos. Es un recurso (o feature) de una clase que se parece un poco a las definiciones de propiedades. Pero tampoco es una propiedad de tipo delegado, como sucede con los eventos en Delphi. Las diferencias, que veremos en breve, se deben principalmente al hecho de que los eventos en .NET permiten la multidifusión.

Concentrémonos ahora en los delegados. Aunque, como he dicho antes, un tipo delegado es un tipo especial de clase, la sintaxis de declaración de delegados es especial:

public delegate void EventHandler(object sender, EventArgs e);

Como puede ver, es muy parecida a la declaración de un método, excepto por la palabra clave delegate. Como se trata de una declaración de tipo, puede incluirse dentro de una clase, pero también podemos situarla directamente dentro de un espacio de nombres, fuera de toda clase. El tipo equivalente en Delphi sería como el siguiente:

type
   EventHandler = procedure (Sender: TObject; E: EventArgs) of object;

Veamos ahora cómo se utiliza un tipo delegado. En primer lugar, hay que comprender que se trata de un tipo de referencia; en definitiva, un tipo delegado es una clase, ¿no? Eche un vistazo al siguiente ejemplo:

// Declaramos un tipo delegado
public delegate void Delegado(string mensaje);

public class PruebaDelegado {
  // Declaración de una variable
  private Delegado variable;

  // Este método es compatible con el tipo delegado anterior
  private void MetodoCompatible(string mensaje) {
    MessageBox.Show(mensaje);
  }

  // Ahora viene la prueba...
  private void Probar() {
    // Creamos un delegado y lo asignamos en
    // la variable que definimos antes en este clase
    // La palabra clave this es innecesaria, las dos veces
    this.variable = new Delegado(this.MetodoCompatible);  // <--

    // Ahora ejecutamos indirectamente el método MetodoCompatible
    // utilizando el delegado como si fuese un puntero al método
    variable("Esto es un atraco");
  }
}

Primero, declaramos un tipo de delegado, que permite apuntar a métodos que reciban una cadena como único parámetro. Luego creamos un campo dentro de una clase de prueba. En el ejemplo no se ve, pero C# inicializa este campo con un puntero nulo. A continuación, hemos definido un método compatible con el tipo delegado. Y finalmente, dentro del método Probar, hemos asignado un valor para el delegado. Compare cómo se asigna un delegado en C# respecto a la asignación de un puntero a método en Delphi:

// C#:
variable = new Delegado(MetodoCompatible);
// Delphi para Win32
variable := MetodoCompatible;

Como el delegado de C# es un tipo de referencia, ha sido necesario usar el operador new (en la siguiente versión de C#, no será necesario, porque el compilador podrá deducirlo del contexto). La siguiente imagen muestra la situación cuando el campo variable ha sido instanciado:


La imagen muestra que una instancia de un tipo delegado tiene (al menos) tres campos internos, cada uno de cuatro bytes en la actual implementación. Esos nombres son orientativos, porque está claro que el tipo delegado no los declara públicamente. El campo que he titulado _target apunta al objeto cuyo método ejecutará el delegado, y methodPtr es un puntero al segmento de código, que apunta a la implementación del método correspondiente. Sin embargo, al programador Delphi le sorprenderá más la presencia del campo _prev. Este campo permite crear listas enlazadas de delegados para que, cuando se "ejecute" el campo que apunta a la cabecera, se ejecuten los métodos asociados a todos estas instancias enlazadas.

Para crear una cadena de delegados se utiliza el método estático Combine, definido en la clase Delegate, que es la clase base que heredan todos los tipos delegados:

  private Delegado variable;

  // Primer método compatible con el tipo delegado
  private void MetodoCompatible1(string mensaje) {
    MessageBox.Show(mensaje);
  }

  // Segundo método compatible con el tipo delegado
  private void MetodoCompatible2(string mensaje) {
    Console.WriteLine(mensaje);
  }

  private void Probar()
  {
    variable = (Delegado) Delegate.Combine(
      new Delegado(MetodoCompatible1),
      new Delegado(MetodoCompatible2));

    // Esta instrucción provoca la ejecución
    // de los dos métodos anteriores
    variable("Esto es otro atraco");
  }

La siguiente imagen ilustra la nueva situación:


Muchas funciones del API nativo de Windows utilizan callbacks o funciones de respuesta, es decir, punteros comunes y corrientes a procedimientos (no punteros a métodos). Este es el caso, por poner un ejemplo, de EnumWindows. Para que se haga una idea de la potencia de .NET, estas funciones pueden ejecutarse sin demasiados aspavientos desde un programa .NET, gracias a una técnica llamada platform invoke, o P/Invoke. Lo interesante es que, a la función "importada" desde una DLL que requiere un puntero de tipo callback, se le pasa un delegado donde va este parámetro de respuesta. .NET se encarga de hacer las traducciones y ajustes correspondientes, sin que tengamos que intervenir.

EVENTOS, HABLANDO CON PROPIEDAD

Un delegado, sin embargo, no es un evento. Para explicar por qué hay que seguir "complicando" las cosas, supondremos que hemos creado una clase de botón, MiBoton, y como es lógico, queremos que cada vez que sea pulsado se ejecuten uno o más métodos, a modo de manejadores de eventos. Para acercarnos un poco a la realidad, supondremos que el tipo delegado es el tipo predefinido por .NET EventHandler, cuya definición hemos visto antes. Supongamos también que, en la primera aproximación, hemos definido una propiedad pública de tipo EventHandler, tal como hace Delphi:

public class MiBoton
{
  // El campo que implementará la propiedad
  private EventHandler click;
  // La propiedad, en sí
  public EventHandler Click
  {
    get { return click; }
    set { click = value; }
  }
  // ... el código de la clase...
}

Los nombres de eventos en .NET no comienzan con On, como en Delphi. En .NET, Click es un evento de la clase de botones, y OnClick es un método virtual protegido que lo dispara. En Delphi, el convenio de nombres funciona a la inversa.

Lo más importante a recordar es que una variable de tipo delegado puede estar apuntando a toda una cadena de valores delegados, es decir, de punteros a métodos. El problema de usar una propiedad para representar un evento es el siguiente:

// En algún lugar de nuestro código,
// asignamos un manejador a la cadena de delegados
button1.Click = new EventHandler(MiManejador);

// En algún otro lugar o módulo, alguien,
// inadvertidamente, elimina nuestro manejador de la lista enlazada
button1.Click = new EventHandler(OtroManejador);

En realidad, en los dos sitios donde hemos asignado un manejador, debíamos haber utilizado Combine:

// Combine reacciona correctamente incluso
// si uno de sus argumentos es un puntero nulo
button1.Click = (EventHandler) Delegate.Combine(
  button1.Click,
  new EventHandler(MiManejador));

// En algún otro lugar o módulo, alguien
// "añade" otro manejador a la lista
button1.Click = (EventHandler) Delegate.Combine(
  button1.Click,
  new EventHandler(OtroManejador);

Aunque esta disciplina resolvería el problema, un programador chapucero podría hacer estragos en la aplicación. Por lo tanto, la solución aportada por C# es declarar el evento como un tipo especial de propiedad:

public class MiBoton
{
  // ¡¡¡Esto es todo lo que necesitamos!!!
  public event EventHandler Click;

  // ... el código de la clase...
}

Esta declaración "automática" de un evento es más o menos equivalente a la siguiente declaración, también correcta:

public class MiBoton
{
  // Un campo para almacenar la lista de delegados
  private EventHandler click;
  // Evento con implementación explícita
  public event EventHandler Click
  {
    add { click = (EventHandler) Delegate.Combine(click, value); }
    remove { click = (EventHandler) Delegate.Remove(click, value); }
  }

  // ... el código de la clase...
}

Ahora todo debe quedar claro: un evento es un tipo especial de propiedad, que en vez de implementarse mediante cláusulas get/set, similares al read/write de Delphi, se implementa mediante cláusulas add/remove. Gracias a esta técnica, ahora es imposible hacer lo siguiente:

  // ¡No compila!
  button1.Click = new EventHandler(MiManejador);

Las operaciones permitidas sobre el evento son las siguientes:

  // Añadimos un manejador a la lista
  button1.Click += new EventHandler(MiManejador);
  // Eliminamos un manejador de la lista
  button1.Click -= new EventHandler(MiManejador);

Muy importante: no se deje engañar por new. A primera vista, parece que la segunda instrucción no será capaz de eliminar el delegado ya existente en la lista. Parece lógico, porque las dos llamadas a new generan en realidad dos instancias con diferentes identidades. El truco está en que la comparación entre instancias de delegados no procede por identidad, sino por valor: se comparan sus punteros internos a métodos.

VIDAS Y HAZAÑAS DE EVENTOS Y DELEGADOS

Hay varios detalles adicionales en relación con los eventos y tipos delegados que debe saber para poder entender algunos de los ejemplos en C# que circulan por este mundo:

  1. Aunque un delegado puede tener los parámetros que nos dé la gana, se recomienda que todos los eventos en .NET, por convenio, tengan un prototipo similar: deben aceptar un parámetro sender de tipo object, un segundo parámetro perteneciente a una clase derivada de EventArgs, y no deben tener valor de retorno.
  2. La implementación de las clases de delegados es algo misteriosa, porque es responsabilidad del JIT (el módulo que traduce el código IL a código nativo) y del entorno de ejecución, o CLR. Esto se manifiesta, por ejemplo, en que no podemos derivar a mano una clase a partir de Delegate. Este "misterio" en realidad es una técnica de encapsulamiento común y corriente, que nos esconde los sucios detalles de la implementación para que no dependamos de ellos. Gracias a ello, la implementación de eventos y delegados en .NET es muy eficiente.
  3. Los métodos Combine y Remove pueden sustituirse por los operadores de adición y resta, o incluso por los correspondientes operadores de asignación compuesta. El ejemplo anterior de implementación explícita de un evento podía haberse escrito así también:
  public event EventHandler Click
  {
    add { click += value; }
    remove { click -= value; }
  }
  1. Los delegados son tipos "inmutables". Por ejemplo, Combine y Remove no afectan el estado de sus operadores, sino que crean una nueva instancia del tipo delegado, si es necesario.
  2. Importante: en Delphi, un puntero a método sólo puede apuntar a un método de un objeto, es decir, a un método de instancia. En C#, los delegados pueden apuntar también a un método estático de una clase. Cuando leí por primera vez el libro de Charles Petzold sobre programación en Windows Forms, no conocía todavía esta posibilidad, y los ejemplos me confundieron bastante.
  3. Los tipos delegados son la base de una técnica muy elegante de .NET para la ejecución asíncrona de métodos.
  4. Normalmente, no merece la pena utilizar la declaración de los métodos de acceso de un evento, pero hay casos en los que sí es interesante. Suponga que ha programado un control parecido a un botón. Es común que el control publique tropecientos eventos. Con la técnica de implementación usual, esto exigirá también tropecientos campos privados para cada instancia del control... campos que en la mayoría de los casos contendrán un puntero vacío. Existe una técnica sencilla, que explican casi todos los libros sobre C#, que consiste en almacenar dentro de la instancia una sola tabla de hash. Si un cliente de la clase añade un manejador de eventos, el correspondiente puntero se añade a la tabla. Así sólo se consume la memoria necesaria, más un pequeño gasto adicional inevitable.

METODOS ANONIMOS VERSUS CLASES ANONIMAS

Es muy fácil programar en C# si disponemos de Visual Studio y utilizamos las superficies visuales de diseño. Pero una de las metas de diseño de este lenguaje es facilitar la programación incluso cuando no se utiliza un entorno RAD de desarrollo. Imagine ahora cuánto hay que teclear para, en tiempo de ejecución, asignar un manejador de evento a un control ya existente. En teoría no es mucho, porque nos basta añadir un nuevo delegado al evento:

button1.Click += new EventHandler(button1_click);

No es mucho código, sino todo lo contrario. Pero hay código "oculto": debemos haber definido antes el método button1_click, en otra sección del fichero de código. Por otra parte, ¿no es aburrido tener siempre que teclear eso de "new EventHandler"?

La versión 2 de C#, conocida como Whidbey, nos trae buenas noticias. En esta versión podremos abreviar el ejemplo anterior de esta forma:

button1.Click += button1_click;

Pero hay más. Supongamos que la implementación del método button1_click es muy sencilla; digamos que sólo muestra un mensaje en un diálogo modal. En vez de definir un método separado para el manejador del evento, podemos utilizar un método anónimo:

button1.Click += delegate { MessageBox.Show("Me han pulsado"); }

¡Estamos definiendo todo un método dentro de una excepción! Se trata de un método anónimo, claro está, y en algunos sentidos se parece a la técnica de las clases anidadas de Java. ¿Qué sucede, hemos dado un paso atrás? Nada de eso. Para empezar, la semántica de un método anónimo es infinitamente más sencilla y predecible que la de una clase anónima anidada de Java. Tenga en cuenta que antes mencionamos cuatro tipos diferentes de clases anidadas, mientras que básicamente hay un solo tipo de método anónimo. Además, la referencia a variables locales o de instancia por parte de un método es más fácil de explicar e implementar que la interacción de una clase anidada de Java con su contexto. El método anónimo que acabamos de ver puede traducirse por el compilador como un método estático de la propia clase donde lo hemos definido. Por último: una clase necesita mucha más información en tiempo de ejecución que un método... incluso en Java.

En el ejemplo anterior, no hemos necesitado declarar la lista de parámetros del delegado, porque no hemos usado estos parámetros dentro del método anónimo. Pero es posible declarar la lista de parámetros de forma explícita:

Application.ThreadException +=
  delegate(object sender, ThreadExceptionEventArgs e) {
    MessageBox.Show(e.Exception.Message);
  }

Por supuesto, hasta que la versión 2 de la plataforma .NET esté en la calle, pueden variar algunos de los detalles concretos de esta técnica.