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)