Delegados y eventos

Concepto de delegado

    Un delegado es un tipo especial de clase cuyos objetos pueden almacenar referencias a uno o más métodos de tal manera que a través del objeto sea posible solicitar la ejecución en cadena de todos ellos.

    Los delegados son muy útiles ya que permiten disponer de objetos cuyos métodos puedan ser modificados dinámicamente durante la ejecución de un programa. De hecho, son el mecanismo básico en el que se basa la escritura de aplicaciones de ventanas en la plataforma .NET. Por ejemplo, si en los objetos de una clase Button que represente a los botones estándar de Windows definimos un campo de tipo delegado, podemos conseguir que cada botón que se cree ejecute un código diferente al ser pulsado sin más que almacenar el código a ejecutar por cada botón en su campo de tipo delegado y luego solicitar la ejecución todo este código almacenado cada vez que se pulse el botón.

    Sin embargo, también son útiles para muchísimas otras cosas tales como asociación de código a la carga y descarga de ensamblados, a cambios en bases de datos, a cambios en el sistema de archivos, a la finalización de operaciones asíncronas, la ordenación de conjuntos de elementos, etc. En general, son útiles en todos aquellos casos en que interese pasar métodos como parámetros de otros métodos.

    Además, los delegados proporcionan un mecanismo mediante el cual unos objetos pueden solicitar a otros que se les notifique cuando ocurran ciertos sucesos. Para ello, bastaría seguir el patrón consistente en hacer que los objetos notificadores dispongan de algún campo de tipo delegado y hacer que los objetos interesados almacenen métodos suyos en dichos campos de modo que cuando ocurra el suceso apropiado el objeto notificador simule la notificación ejecutando todos los métodos así asociados a él.

Definición de delegados

    Un delegado no es más que un tipo especial de subclase System.MulticastDelegate. Sin embargo, para definir estas clases no se puede utilizar el mecanismo de herencia normal sino que ha de seguirse la siguiente sintaxis especial:


<modificadores> delegate <tipoRetorno> <nombreDelegado> (<parámetros>);

    <nombreDelegado> será el nombre de la clase delegado que se define, mientras que <tipoRetorno> y <parámetros> se corresponderán, respectivamente, con el tipo del valor de retorno y la lista de parámetros de los métodos cuyos códigos puede almacenar en su interior los objetos de ese tipo delegado (objetos delegados)

    Un ejemplo de cómo definir un delegado de nombre Deleg cuyos objetos puedan almacenar métodos que devuelvan un string y tomen como parámetro un int es:


delegate void Deleg(int valor);

    Cualquier intento de almacenar en este delegado métodos que no tomen sólo un int como parámetro o no devuelvan un string producirá un error de compilación o, si no pudiese detectarse al compilar, una excepción de tipo System.ArgumentNullException en tiempo de ejecución. Esto puede verse con el siguiente programa de ejemplo:


using System;
using System.Reflection;
public delegate void D();
public class ComprobaciónDelegados     
{
 public static void Main()
 {
  Type t = typeof(ComprobaciónDelegados);
  MethodInfo m = t.GetMethod("Método1");
  D obj  = (D) Delegate.CreateDelegate(typeof(D), m);
  obj();
 }
 
 public static void Método1()
 {
  Console.WriteLine("Ejecutado Método1");
 }
 
 public static void Método2(string s)
 {
  Console.WriteLine("Ejecutado Método2");
 }
}

    Lo que se hace en el método Main() de este programa es crear a partir del objeto Type que representa al tipo ComprobaciónDelegados un objeto System.Reflection.MethodInfo que representa a su método Método1. Como se ve, para crear el objeto Type se utiliza el operador typeof ya estudiado, y para obtener el objeto MethodInfo se usa su método GetMethod() que toma como parámetro una cadena con el nombre del método cuyo MethodInfo desee obtenerse. Una vez conseguido, se crea un objeto delegado de tipo D que almacene una referencia al método por él representado a través del método CreateDelegate() de la clase Delegate y se llama dicho objeto, lo que muestra el mensaje:


 Ejecutando Método1

    Aunque en vez de obtener el MethodInfo que representa al Método1 se hubiese obtenido el que representa al Método2 el compilador no detectaría nada raro al compilar ya que no es lo bastante inteligente como para saber que dicho objeto no representa a un método almacenable en objetos delegados de tipo D. Sin embargo, al ejecutarse la aplicación el CLR sí que lo detectaría y ello provocaría una ArgumentNullException

    Esto es un diferencia importante de los delegados respecto a los punteros a función de C/C++ (que también pueden almacenar referencias a métodos), ya que con estos últimos no se realizan dichas comprobaciones en tiempo de ejecución y puede terminar ocurriendo que un puntero a función apunte a un método cuya signatura o valor de retorno no se correspondan con los indicados en su definición, lo que puede ocasionar que el programa falle por causas difíciles de detectar.

    Las definiciones de delegados también pueden incluir cualquiera de los modificadores de accesibilidad válidos para una clase, ya que al fin y al cabo los delegados son clases. Es decir, todos pueden incluir los modificadores public e internal, y los se definan dentro de otro tipo también pueden incluir protected, private y protected internal.

Manipulación de objetos delegados

    Un objeto de un tipo delegado se crea exactamente igual que un objeto de cualquier clase sólo que en su constructor ha de pasársele el nombre del método cuyo código almacenará. Este método puede tanto ser un método estático como uno no estático. En el primer caso se indicaría su nombre con la sintaxis <nombreTipo>.<nombreMétodo>, y en el segundo se indicaría con <objeto>.<nombreMétodo>

    Para llamar al código almacenado en el delegado se usa una sintaxis similar a la de las llamadas a métodos, sólo que no hay que prefijar el objeto delegado de ningún nombre de tipo o de objeto y se usa simplemente <objetoDelegado>(<valoresParámetros>)

    El siguiente ejemplo muestra cómo crear un objeto delegado de tipo D, asociarle el código de un método llamado F y ejecutar dicho código a través del objeto delegado:


using System;
delegate void D(int valor);
class EjemploDelegado
{
 public static void Main()
 {
  D objDelegado = new D(F);
  objDelegado(3);
 }
 
 public static void F(int x)           
 {
  Console.WriteLine( "Pasado valor {0} a F()", x);
 }
}

    La ejecución de este programa producirá la siguiente salida por pantalla:


 Pasado valor 3 a F()

    Nótese que para asociar el código de F() al delegado no se ha indicado el nombre de este método estático con la sintaxis <nombreTipo>.<nombreMétodo> antes comentada. Esto se debe a que no es necesario incluir el <nombreTipo>. cuando el método a asociar a un delegado es estático y está definido en el mismo tipo que el código donde es asociado

    En realidad un objeto delegado puede almacenar códigos de múltiples métodos tanto estáticos como no estáticos de manera que una llamada a través suya produzca la ejecución en cadena de todos ellos en el mismo orden en que se almacenaron en él. Nótese que si los métodos devuelven algún valor, tras la ejecución de la cadena de llamadas sólo se devolverá el valor de retorno de la última llamada.

    Además, cuando se realiza una llamada a través de un objeto delegado no se tienen en cuenta los modificadores de visibilidad de los métodos que se ejecutarán, lo que permite llamar desde un tipo a métodos privados de otros tipos que estén almacenados en un delegado por accesible desde el primero tal y como muestra el siguiente ejemplo:


using System;
public delegate void D();
class A
{
 public static D obj;
 public static void Main()
 {
  B.AlmacenaPrivado();
  obj();
 }
}
class B
{
 private static void Privado()
 {
  Console.WriteLine("Llamado a método privado");
 }
 
 public static void AlmacenaPrivado()
 {
  A.obj += new D(Privado);
 }
}

    La llamada a AlmacenaPrivado en el método Main() de la clase A provoca que en el campo delegado obj de dicha clase se almacene una referencia al método privado Privado() de la clase B, y la instrucción siguiente provoca la llamada a dicho método privado desde una clase externa a la de su definición como demuestra la salida del programa:


 Llamado a método privado

    Para añadir nuevos métodos a un objeto delegado se le aplica el operador += pasándole como operando derecho un objeto delegado de su mismo tipo (no vale de otro aunque admita los mismos tipos de parámetros y valor de retorno) que contenga los métodos a añadirle, y para quitárselos se hace lo mismo pero con el operador -=. Por ejemplo, el siguiente código muestra los efectos de ambos operadores:


  using System;
 
  delegate void D(int valor);
 
  class EjemploDelegado
  {
   public string Nombre;
   EjemploDelegado(string nombre)
   {
    Nombre = nombre;
   }
   
   public static void Main()
   {
    EjemploDelegado obj1 += new EjemploDelegado("obj1");
    D objDelegado = new D(f);
    objDelegado +=  new D(obj1.g);
    objDelegado(3);
    objDelegado -= new D(obj1.g);
    objDelegado(5);
   }
   
   public void g(int x)
   {
    Console.WriteLine("Pasado valor {0} a g() en objeto {1}", x, Nombre);
   }
   
   public static void f(int x)
   {
    Console.WriteLine( "Pasado valor {0} a f()", x);
   }
 

La salida producida por pantalla por este programa será:


Pasado valor 3 a f()
 
 Pasado valor 3 a g() en objeto obj1
 
 Pasado valor 5 a f()

    Como se ve, cuando ahora se hace la llamada objDelegado(3) se ejecutan los códigos de los dos métodos almacenados en objDelegado, y al quitársele luego uno de estos códigos la siguiente llamada sólo ejecuta el código del que queda. Nótese además en el ejemplo como la redefinición de + realizada para los delegados permite que se pueda inicializar objDelegado usando += en vez de =. Es decir, si uno de los operandos de + vale  null no se produce ninguna excepción, sino que tan sólo no se añade ningún método al otro. 

    Hay que señalar que un objeto delegado vale null si no tiene ningún método asociado, ya sea porque no se ha llamado aún a su constructor o porque los que tuviese asociado se le hayan quitado con -=. Así, si al Main() del ejemplo anterior le añadimos al final:


objDelegado -= new D(f);
objDelegado(6);

    Se producirá al ejecutarlo una excepción de tipo System.NullReferenceException indicando que se ha intentado acceder a una referencia nula.

    También hay que señalar que para que el operador -= funcione se le ha de pasar como operador derecho un objeto delegado que almacene algún método exactamente igual al método que se le quiera quitar al objeto delegado de su lado izquierdo. Por ejemplo, si se le quiere quitar un método de un cierto objeto, se le ha de pasar un objeto delegado que almacene ese método de ese mismo objeto, y no vale que almacene ese método pero de otro objeto de su mismo tipo. Por ejemplo, si al Main() anterior le añadimos al final:


objDelegado -= new g(obj1.g);
objDelegado(6);

    Entonces no se producirá ninguna excepción ya que el -= no eliminará ningún método de objDelegado debido a que ese objeto delegado no contiene ningún método g() procedente del objeto obj1. Es más, la salida que se producirá por pantalla será:


 Pasado valor 3 a f()
 
 Pasado valor 3 a g() en objeto obj1
 
 Pasado valor 5 a f()
 
 Pasado valor 6 a f()

Delegados y eventos
José Antonio González Seco

José Antonio es experto en tecnologias Microsoft. Imparte cursos y conferencias en congresos sobre C# y .NET en Universidades de toda España (Sevilla, Barcelona, San Sebastián, Valencia, Oviedo, etc.) en representación de grandes empresas como Microsoft.
Fecha de alta:16/10/2006
Última actualizacion:16/10/2006
Visitas totales:35770
Valorar el contenido:
Últimas consultas realizadas en los foros
Últimas preguntas sin contestar en los foros de devjoker.com