Zona HTML Zona Java Zona PHP Zona ASP Zona Bases de datos
Inicio > Tutoriales > Lenguajes orientados a objeto > C# > El lenguaje de programación C#
-Tutoriales

El lenguaje de programación C#


Tema 12: 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 mecanismos 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 string Deleg(int valor);

Los objetos delegados de este tipo sólo podrán almacenar códigos de métodos que no devuelvan nada y tomen un único parámetro de tipo int. Cualquier intento de almacenar métodos con otras características producirá un error de compilación o, si no puede detectarse al compilar, una excepción de tipo System.ArgumentNullException en tiempo de ejecución tal y como muestra 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:

Ejecutado 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

Ésto 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()");
  }
}

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()

. La clase System.MulticastDelegate

Ya se ha dicho que la sintaxis especial de definición de delegados no es más que una forma especial definir subclases de System.MulticastDelegate. Esta clase a su vez deriva de System.Delegate, que representa a objetos delegados que sólo puede almacenar un único método. Por tanto, todos los objetos delegado que se definan contarán con los siguientes miembros comunes heredados de estas clases:

  • object Target: Propiedad de sólo lectura que almacena el objeto al que pertenece el último método añadido al objeto delegado. Si es un método de clase vale null.
  • MethodInfo Method: Propiedad de sólo lectura que almacena un objeto System.Reflection.MethodInfo con información sobre el último método añadido al objeto (nombre, modificadores, etc.) Para saber cómo acceder a estos datos puede consultar la documentación incluida en el SDK sobre la clase MethodInfo
  • Delegate[] getInvocationList(): Permite acceder a todos los métodos almacenados en un delegado, ya que devuelve una tabla cuyos elementos son delegados cada uno de los cuales almacenan uno, y sólo uno, de los métodos del original. Estos delegados se encuentran ordenados en la tabla en el mismo orden en que sus métodos fueron fue almacenados en el objeto delegado original.

    Este método es especialmente útil porque a través de la tabla que retorna se pueden hacer cosas tales como ejecutar los métodos del delegado en un orden diferente al de su almacenamiento, procesar los valores de retorno de todas las llamadas a los métodos del delegado original, evitar que una excepción en la ejecución de uno de los métodos impida la ejecución de los demás, etc.

Aparte de estos métodos de objeto, la clase System.MulticastDelegate también cuenta con los siguientes métodos de tipo de uso frecuente:

  • static Delegate Combine(Delegate fuente, Delegate destino): Devuelve un nuevo objeto delegado que almacena la concatenación de los métodos de fuente con los de destino. Por tanto, nótese que estas tres instrucciones son equivalentes:
    objDelegado += new D(obj1.g);
    objDelegado = objDelegado + new D(obj1.g);
    objDelegado = (D) MulticastDelegate.Combine(objDelegado, new D(obj1.g);

    Es más, en realidad el compilador de C# lo que hace es convertir toda aplicación del operador + entre delegados en una llamada a Combine() como la mostrada.

    Hay que tener cuidado con los tipos de los delegados a combinar ya que han de ser exáctamente los mismos o si no se lanza una System.ArgumentException, y ello ocurre aún en el caso de que dichos sólo se diferencien en su nombre y no en sus tipos de parámetros y valor de retorno.

  • static Delegate Combine(Delegate[] tabla): Devuelve un nuevo delegado cuyos métodos almacenados son la concatenación de todos los de la lista que se le pasa como parámetro y en el orden en que apareciesen en ella. Es una buena forma de crear delegados con muchos métodos sin tener que aplicar += varias veces.

Todos los objetos delegados de la tabla han de ser del mismo tipo, pues si no se produciría una System.ArgumentException.

  • static Delegate Remove(Delegate original, Delegate aBorrar): Devuelve un nuevo delegado cuyos métodos almacenados son el resultado de eliminar de original los que tuviese aBorrar. Por tanto, estas instrucciones son equivalentes:
    objDelegado -= new D(obj1.g);
    objDelegado - objDelegado - new D(obj1.g);
    objDelegado = (D) MulticastDelegate.Remove(objDelegado, new D(obj1.g);

    Nuevamente, lo que hace el compilador de C# es convertir toda aplicación del operador - entre delegados en una llamada a Remove() como la mostrada. Por tanto, al igual que con -=, para borrar métodos de objeto se ha de especificar en aBorrar un objeto delegado que contenga referencias a métodos asociados a exáctamente los mismos objetos que los almacenados en original.

  • static Delegate CreateDelegate (Type tipo, MehodInfo método): Ya se usó este método en el ejemplo de comprobación de tipos del epígrafe "Definición de delegados" de este mismo tema. Como recordará pemrite crear dinámicamente objetos delegados, ya que devuelve un objeto delegado del tipo indicado que almacena una referencia al método representado por su segundo parámetro.

. Llamadas asíncronas

La forma de llamar a métodos que hasta ahora se ha explicado realiza la llamada de manera síncrona, lo que significa que la instrucción siguiente a la llamada no se ejecuta hasta que no finalice el método llamado. Sin embargo, a todo método almacenado en un objeto delegado también es posible llamarde manera asíncrona a través de los métodos del mismo, lo que consiste en que no se espera a que acabe de ejecutarse para pasar a la instrucción siguiente a su llamada sino que su ejecución se deja en manos de un hilo aparte que se irá ejecutándolo en paralelo con el hilo llamante.

Por tanto los delegados proporcionan un cómodo mecanismo para ejecutar cualquier método asíncronamente, pues para ello basta introducirlo en un objeto delegado del tipo apropiado. Sin embargo, este mecanismo de llamada asíncrona tiene una limitación, y es que sólo es válido para objetos delegados que almacenen un único método.

Para hacer posible la llamadas asíncronas, aparte de los métodos heredados de System.MulticastDelegate todo objeto delegado cuenta con estos otros dos métodos que el compilador define a su medida en al clase en que traduce la definición de su tipo:

IAsyncResult BeginInvoke( <parámetros> , AsyncCallback cb,  Object o)
<tipoRetorno> EndInvoke(<parámetrosRefOut>, IASyncResult ar)

BeginInvoke() crea un hilo que ejecutará los métodos almacenados en el objeto delegado sobre el que se aplica con los parámetros indicados en <parámetros> y devuelve un objeto IAsyncResult que almacenará información relativa a ese hilo (por ejemplo, a través de su propiedad de sólo lectura bool IsComplete puede consultarse si ha terminado su labor) Sólo tiene sentido llamarlo si el objeto delegado sobre el que se aplica almacena un único método, pues si no se lanza una System.ArgumentException.

El parámetro cb de BeginInvoke() es un objeto de tipo delegado que puede almacenar métodos a ejecutar cuando el hilo antes comentado finalice su trabajo. A estos métodos el CLR les pasará automáticamente como parámetro el IAsyncResult devuelto por BeginInvoke(), estándo así definido el delegado destinado a almacenarlos:

public delegate void ASyncCallback(IASyncResult obj);

Por su parte, el parámetro o de BeginInvoke puede usarse para almacenar cualquier información adicional que se considere oportuna. Es posible acceder a él a través de la propiedad object AsyncState del objeto IAsyncResult devuelto por BeginInvoke()

En caso de que no se desee ejecutar ningún código especial al finalizar el hilo de ejecución asíncrona o no desee usar información adicional, puede darse sin ningún tipo de problema el valor null a los últimos parámetros de BeginInvoke() según corresponda.

Finalmente, EndInvoke() se usa para recoger los resultados de la ejecución asíncrona de los métodos iniciada a través BeginInvoke() Por ello, su valor de retorno es del mismo tipo que los métodos almacenables en el objeto delegado al que pertenece y en <parámetrosRefOut> se indican los parámetros de salida y por referencia de dichos métodos. Su tercer parámetro es el IAsyncResult devuelto por el BeginInvoke() que creó el hilo cuyos se solicita recoger y se usa precisamente para identificarlo. En caso de que ese hilo no haya terminado aún de hacer las llamadas se esperará a que lo haga.

Para ilustrar mejor el concepto de llamadas asíncronas, el siguiente ejemplo muestra cómo encapsular en un objeto delegado un método F() para ejecutarlo asíncronamente:

D objDelegado =  new  D (F);
IAsyncResult hilo = objDelegado.BeginInvoke(3, new AsyncCallback(M),
                                            "prueba");
// ... Hacer cosas
objDelegado.EndInvoke(hilo);

Donde el método M ha sido definido en la misma clase que este código así:

public static void M(IAsyncResult obj)
{
  Console.WriteLine("Llamado a M() con {0}", obj.AsyncState);
}

Si entre el BeginInvoke() y el EndInvoke() no hubiese habido ninguna escritura en pantalla, la salida del fragmento de código anterior sería:

Pasado valor 3 a F()
Llamado a M() con prueba

La llamada a BeginInvoke() lanzará un hilo que ejecutará el método F() almacenado en objDelegado, pero mientras tanto también seguirá ejecutándose el código del hilo desde donde se llamó a BeginInvoke() Sólo tras llamar a EndInvoke() se puede asegurar que se habrá ejecutado el código de F(), pues mientras tanto la evolución de ambos hilos es prácticamente indeterminable ya que depende del cómo actúe el planificador de hilos.

Aún si el hilo llamador modifica el valor de alguno de los parámetros de salida o por referencia de tipos valor, el valor actualizado de éstos no será visible para el hilo llamante hasta no llamar a EndInvoke() Sin embargo, el valor de los parámetros de tipos referencia sí que podría serlo. Por ejemplo, dado un código como:

int x=0;
Persona p = new Persona("Josan", "7361928-E", 22);

IAsyncResult res  = objetoDelegado.BeginInvoke(ref x, p, null, null);
// Hacer cosas...
objetoDelegado.EndInvoke(ref x, res);

Si en un punto del código comentado con // Hacer cosas... donde el hilo asíncrono ya hubiese modificado los contenidos de x y p se intentase leer los valores de estas variables sólo se leería el valor actualizado de p, mientras el de x no se vería hasta después de la llamada a EndInvoke()

Por otro lado, hay que señalar que si durante la ejecución asíncrona de un método se produce alguna excepción, ésta no sería notificada pero provocaría que el hilo asíncrono abortase. Si posteriormente se llamase a EndInvoke() con el IAsyncResult asociado a dicho hilo, se relanzaría la excepción que produjo el aborto y entonces podría tratarse.

Para optimizar las llamadas asíncronas es recomendable marcar con el atributo OneWay definido en System.Runtime.Remoting.Messaging los métodos cuyo valor de retorno y valores de parámetros de salida no nos importen, pues ello indica a la infraestructura encargada de hacer las llamadas asíncronas que no ha de considerar. Por ejemplo:

[OneWay] public void Método()
{}

Ahora bien, hay que tener en cuenta que hacer esto implica perder toda posibilidad de tratar las excepciones que pudiese producirse al ejecutar asíncronamente el método atribuido, pues con ello llamar a EndInvoke() dejaría de relanzar la excepción producida.

Por último, a modo de resumen a continuación se indican cuáles son los patrones que pueden seguirse para recoger los resultados de una llamada asíncrona:

  • Detectar si la llamada asíncrona ha finalizado mirando el valor de la propiedad IsComplete del objeto IAsyncResult devuelto por BeginInvoke() Cuando sea así, con EndInvoke() puede recogerse sus resultados.
  • Pasar un objeto delegado en el penúltimo parámetro de BeginInvoke() con el método a ejecutar cuando finalice el hilo asíncrono, lo que liberaría al hilo llamante de la tarea de tener que andar mirando si ha finalizado o no.

    Si desde dicho método se necesitase acceder a los resultados del método llamado podría accederse a ellos a través de la propiedad AsyncDelegate del objeto IAsyncResult que recibe. Esta propiedad contiene el objeto delegado al que se llamó, aunque se muestra a continuación antes de acceder a ella hay que convertir el parámetro IAsyncResult de ese método en un AsyncResult:

    public static void M(IAsyncResult iar)
    {
      D objetoDelegado = (D) ((AsyncResult iar)).AsyncDelegate;
      
      // A partir de aquí podría llamarse a EndInvoke() a
      // través de objetoDelegado
    }

. Implementación interna de los delegados

Cuando hacemos una definición de delegado de la forma:

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

El compilador internamente la transforma en una definición de clase de la forma:

<modificadores> class <nombre>:System.MulticastDelegate
{
  private object _target;
  private int _methodPtr;
  private MulticastDelegate _prev;
  
  public <nombre>(object objetivo, int punteroMétodo)
  {...}
  
  public virtual <tipoRetorno> Invoke(<parámetros>)
  {...}
  
  public virtual IAsyncResult BeginInvoke(<parámetros>, AsyncCallback cb,
                                          Object o)
  {...}
  
  public virtual <tipoRetorno> EndInvoke(<parámetrosRefOut>,IASyncResult ar)
  {...}
}

Lo primero que llama la atención al leer la definición de esta clase es que su constructor no se parece en absoluto al que hemos estado usando hasta ahora para crear objetos delegado. Esto se debe a que en realidad, a partir de los datos especificados en la forma de usar el constructor que el programador utiliza, el compilador es capaz de determinar los valores apropiados para los parámetros del verdadero constructor, que son:

  • object objetivo contiene el objeto al cual pertenece el método especificado, y su valor se guarda en el campo _target. Si es un método estático almacena null.
  • int punteroMétodo contiene un entero que permite al compilador determinar cuál es el método del objeto al que se desea llamar, y su valor se guarda en el campo _methodPtr. Según donde se haya definido dicho método, el valor de este parámetro procederá de las tablas MethodDef o MethodRef de los metadatos.

El campo privado _prev de un delegado almacena una referencia al delegado previo al mismo en la cadena de métodos. En realidad, en un objeto delegado con múltiples métodos lo que se tiene es una cadena de objetos delegados cada uno de los cuales contiene uno de los métodos y una referencia (en _prev) a otro objeto delegado que contendrá otro de los métodos de la cadena.

Cuando se crea un objeto delegado con new se da el valor null a su campo _prev para así indicar que no pertenece a una cadena sino que sólo contiene un método. Cuando se combinen dos objetos delegados (con + o Delegate.Combine()) el campo _prev del nuevo objeto delegado creado enlazará a los dos originales; y cuando se eliminen métodos de la cadena (con - o Delegate.Remove()) se actualizarán los campos _prev de la cadena para que salten a los objetos delegados que contenían los métodos eliminados.

Cuando se solicita la ejecución de los métodos almacenados en un delegado de manera asíncrona lo que se hace es llamar al método Invoke() del mismo. Por ejemplo, una llamada como esta:

objDelegado(49);

Es convertida por el compilador en:

objDelegado.Invoke(49);

Aunque Invoke() es un método público, C# no permite que el programador lo llame explícitamente. Sin embargo, otros lenguajes gestionados sí que podrían permitirlo.

El método Invoke() se sirve de la información almacenada en _target, _methodPtr y _prev, para determinar a cuál método se ha de llamar y en qué orden se le ha de llamar. Así, la implementación de Invoke() será de la forma:

public virtual <tipoRetorno> Invoke(<parámetros>)
{
  if (_prev!=null)
    _prev.Invoke(<parámetros>);
  
  return _target._methodPtr(<parámetros>);
}

Obviamente la sintaxis _target.methodPtr no es válida en C#, ya que _methodPtr no es un método sino un campo. Sin embargo, se ha escrito así para poner de manifiesto que lo que el compilador hace es generar el código apropiado para llamar al método perteneciente al objeto indicado en _target e identificado con el valor de _methodPtr

Nótese que la instrucción if incluida se usa para asegurar que las llamadas a los métodos de la cadena se hagan en orden: si el objeto delegado no es el último de la cadena. (_prev!=null) se llamará antes al método Invoke() de su predecesor.

Por último, sólo señalar que, como es lógico, en caso de que los métodos que el objeto delegado pueda almacenar no tengan valor de retorno (éste sea void), el cuerpo de Invoke() sólo varía en que la palabra reservada return es eliminada del mismo.

. Eventos

. Concepto de evento

Un evento es una variante de las propiedades para los campos cuyos tipos sean delegados. Es decir, permiten controlar la forman en que se accede a los campos delegados y dan la posibilidad de asociar código a ejecutar cada vez que se añada o elimine un método de un campo delegado.

. Sintaxis básica de definición de delegados

La sintaxis básica de definición de un evento consiste en definirlo como cualquier otro campo con la única peculiaridad de que se le ha de anteponer la palabra reservada event al nombre de su tipo (que será un delegado) O sea, se sigue la sintaxis:

<modificadores> event <tipoDelegado> <nombreEvento>;

Por ejemplo, para definir un evento de nombre Prueba y tipo delegado D se haría:

public event D Prueba;

También pueden definirse múltiples eventos en una misma línea separando sus nombres mediante comas. Por ejemplo:

public event D Prueba1, Prueba2;

Desde código ubicado dentro del mismo tipo de dato donde se haya definido el evento se puede usar el evento tal y como si de un campo delegado normal se tratase. Sin embargo, desde código ubicado externamente se imponen una serie de restricciones que permite controlar la forma en que se accede al mismo. Éstas son:

  • No se le puede aplicar los métodos heredados de System.MulticastDelegate.
  • Sólo se le puede aplicar dos operaciones: añadido de métodos con += y eliminación de métodos con -=. De este modo se evita que se use sin querer = en vez de += ó -= y se sustituyan todos los métodos de la lista de métodos del campo delegado por otro que en realidad se le quería añadir o quitar (si ese otro valiese null, ello incluso podría provocar una System.NullReferenceException)
  • No es posible llamar a los métodos almacenados en un campo delegado a través del mismo. Esto permite controlar la forma en que se les llama, ya que obliga a que la llamada tenga que hacerse a través de algún método público definido en la definición del tipo de dato donde el evento fue definido.

. Sintaxis completa de definición de delegados

La verdadera utilidad de un evento es que permite controlar la forma en que se asocian y quitan métodos de los objetos delegados con += y -=. Para ello se han de definir con la siguiente sintaxis avanzada:

<modificadores> event <tipoDelegado> <nombreEvento>
{
  add
  {
    <códigoAdd>
  }
  remove
  {
    <códigoRemove>
  }
}

Con esta sintaxis no pueden definirse varios eventos en una misma línea como ocurría con la básica. Su significado es el siguiente: cuando se asocie un método con += al evento se ejecutará el <códigoAdd>, y cuando se le quite alguno con -= se ejecutará el <códigoRemove>. Esta sintaxis es similar a la de los bloques set/get de las propiedades pero con una importante diferencia: aunque pueden permutarse las secciones add y remove, es obligatorio incluir siempre a ambas.

La sintaxis básica es en realidad una forma abreviada de usar la avanzada. Así, la definición public event D Prueba(int valor); la interpretaría el compilador como:

private D prueba

public event D  Prueba
{
  [MethodImpl(MethodImlOptions.Synchronized)]
  add
  {
    prueba = (D) Delegate.Combine(prueba, value);
  }
  
  [MethodImpl(MethodImlOptions.Synchronized)]
  remove
  {
    prueba = (D) Delegate.Remove(prueba, value);
  }
}

Es decir, el compilador definirá un campo delegado privado y códigos para add y remove que hagan que el uso de += y -= sobre el evento tenga el efecto que normalmente tendrían si se aplicasen directamente sobre el campo privado. Como se ve, dentro de estos métodos se puede usar value para hacer referencia al operando derecho de los operadores += y -=. El atributo System.Runtime.InteropServices.MethodImpl que precede a los bloques add y remove sólo se incluye para asegurar que un cambio de hilo no pueda interrumpir la ejecución de sus códigos asociados.

Las restricciones de uso de eventos desde códigos externos al tipo donde se han definido se deben a que en realidad éstos no son objetos delegados sino que el objeto delegado es el campo privado que internamente define el compilador. El compilador traduce toda llamada al evento en una llamada al campo delegado. Como este es privado, por eso sólo pueda accederse a él desde código de su propio tipo de dato.

En realidad, el compilador internamente traduce las secciones add y remove de la definición de un evento en métodos de la forma:

void add_<nombreEvento>(<tipoDelegado> value)
void remove_<nombreEvento>(<tipoDelegado> value)

Toda aplicación de += y -= a un evento no es convertida en una llamada al campo privado sino en una llamada al método add/remove apropiado, como se puede observar analizando el MSIL de cualquier fuente donde se usen += y -= sobre eventos. Además, como estos métodos devuelven void ése será el tipo del valor devuelto al aplicar += ó -= (y no el objeto asignado), lo que evitará que código externo al tipo donde se haya definido el evento pueda acceder directamente al campo delegado privado.

Si en vez de la sintaxis básica usamos la completa no se definirá automáticamente un campo delegado por cada evento que se defina, por lo que tampoco será posible hacer referencia al mismo desde código ubicado en la misma clase donde se ha definido. Sin embargo ello permite que el programador pueda determinar, a través de secciones add y remove, cómo se almacenarán los métodos. Por ejemplo, para ahorrar memoria se puede optar por usar un diccionario donde almacenar los métodos asociados a varios eventos de un mismo objeto en lugar de usar un objeto delegado por cada uno.

Dado que las secciones add y remove se traducen como métodos, los eventos también podrán participar en el mecanismo de herencia y redefiniciones típico de los métodos. Es decir, en <modificadores> aparte de modificadores de acceso y el modificador static, también se podrán incluir los modificadores relativos a herencia. En este sentido hay que precisar algo: un evento definido como abstract ha de definirse siempre con la sintaxis básica (no incluirá secciones add o remove)

 
Patrocinados
 

Copyright © 1999-2010 Programación en castellano. Todos los derechos reservados.
Formulario de Contacto - Datos legales - Publicidad

diseño y desarrollo web por Color Vivo Internet. Un proyecto de los Hermanos Carrero