jueves, 29 de mayo de 2014

Delegados en C# - Parte 1: Introducción

Tabla de Contenido

0. Introducción
1. Introducción a los Delegados en C#
1.1 ¿Qué es un delegado?
1.2 Función de los delegados
1.3 Declaración de un delegado en C#
1.3.1 Ejemplos básicos de delegados
2. Ejemplo Extendido de Delegados
2.1 Acción del compilador y la CLR en los delegados
3. Uso de Delegados
3.1 Delegados como argumentos de métodos
3.2 Multicast
3.2.1 Proceso largo
4. Sobrecarga
5. Covarianza y Contra-varianza
5.1 Covarianza
5.2 Contra-varianza
6. Métodos Plug-in
7. Delegados vs Interfaces
8. Conclusiones
9. Glosario
10. Literatura & Enlaces

0. Introducción

Empiezo una serie de artículos que estará centrada en otro de los tipos disponibles en el lenguaje C#: los delegados. Esta primera parte será consistirá en la definición esencial, especificaciones de uso con métodos estáticos y métodos de instancia. También se hará hincapié en hablar acerca de varios conceptos de manipulación con el compilador de C# y la CLR: generación de clases asociadas a delegados, representación en código intermedio, y reflection. Además, será incluida la discusión sobre la utilidad de los delegados en programación robusta, los principios que gobiernan a los delegados, y finalmente un breve contraste con las funciones puntero de lenguajes como C y C++.

1. Introducción a los Delegados en C#

Empecemos por explorar los esenciales de los delegados en el lenguaje de programación C#.

1.1 ¿Qué es un delegado?

En términos simples y llanos, podemos definir a un delegado como un objeto que apunta a (utilizo este término debido a la tradición con el uso de apuntadores en C/C++) un método.

Otra definición análoga, consiste en definir a un delegado como un encapsulador o envoltura de métodos estáticos o de instancia.

Los delegados, como acabamos de decir, representan a métodos. Por lo tanto, es evidente que deben poseer características de éstos, como su firma:
  • Lista de parámetros, y
  • Tipo de retorno.

1.2 Función de un delegado

La función de un delegado consiste en independizar la invocación de métodos en tiempo de ejecución. Es decir, que dinámicamente podamos asignar implementaciones específicas a otros algoritmos (por ejemplo, en el algoritmo de ordenar elementos de datos, el cual es independiente del compararlos). Esto genera flexibilidad en la invocación dinámica de métodos:
  • Pasar métodos como funciones para hacer callback.
  • Creación de métodos anónimos (los veremos más adelante con detalle) para resoluciones ad-hoc en código cliente.

1.3 Declaración de un delegado en C#

Para declarar un delegado en el lenguaje de programación C#, utilizamos la siguiente sintaxis abstracta:

{modificador acceso} delegate {tipo retorno} {identificador}({lista argumentos});

Descripción puntual:
  • {modificador acceso}: cualquier de los modificadores de acceso disponibles (Modificadores de Acceso en C#).
  • delegate: Palabra reservada delegate [7].
  • {tipo retorno}: Cualquier tipo de retorno (bilbioteca de clases o uno definido por el programador).
  • {identificador}: Formación de un identificador de lenguaje válido.
  • {lista argumentos}: Lista de parámetros del delegado.

1.3.1 Ejemplos básicos de delegados

A continuación algunos ejemplos básicos de declaración de delgados:

public delegate int Calcular(int x, int y);

public delegate double OperacionMatematica(double x, double y);

En la primera definición, hemos creado un delegado -Calcular- que recibe como entrada dos argumentos de tipo int, y retorna un tipo entero int.

De forma análoga, el delegado OperacionMatematica espera como parámetros dos tipos double, y retorno uno de la misma naturaleza.

Desde código cliente podemos crear instancias de la siguiente manera:

public static int Sumar(int a, int b)
{
return a + b;
}

public static double Maximo(double a, double b)
{
if ( a > b)
{
return a;
}

return b;
}

public static void Main()
{
Calcular sumar = new Calcular(Sumar);

OperacionMatematica maximo = Maximo;
}

Podemos crear una instancia de un delegado utilizando el operador new [9] (de modo igual a que lo hacemos con clases).

Otra manera, sencilla es utilizar la conversión de métodos de grupos (cfr. Tipos Genéricos en C# - Parte 8: Delegados Genéricos) para delegados no-genéricos.

1.4 Delegados en el Framework .NET

Todos los delegados declarados en la biblioteca de clases del Framework .NET y los propios personalizados por el programdor heredan de la clase System.Delegate [15]. A lo anterior, también hay que sumar que la operación de herencia no está permitida, debido a que los tipos delegados se hayan sellados (marcados con sealed), inclusive, no es posible heredar de la clase base Delegate. Hagamos una prueba al respecto:

// Esta declaración no está permitida.
// Generá el error CS0644:
internal class HerenciaDelegate : System.Delegate
{
// Implementación
}


Resultado:

error CS0644: `Articulos.Cap03.HerenciaDelegate' cannot derive from special class `System.Delegate'

Observamos que se generá el error CS0644 [16], que corresponde con la restricción de de herencia de la clase especial System.Delegate.

2. Ejemplo Extendido de Uso de Delegados

En este ejemplo extendido de delegados vamos a ver varios conceptos relacionados con la declaración, uso, manipulación de métodos estáticos y métodos de instancia; los elementos anteriores (como otros) nos llevarán a introducir la generación detrás de cámaras de clases a partir de delegados por parte del compilador y la CLR.

Empecemos por la declaración de clase base de trabajo y del delegado en su cuerpo de definición: (También se incluirá el código cliente.)

Archivo Conjunto.cs:


El delegado Presentador contiene la firma:

public delegate void Presentador(Object valor, Int32 item, Int32 numeroElementos);

Esta firma ha sido modelada para la generación de diferentes presentaciones:
  • PresentacionConsola: Genera la presentación sobre la consola (salida estándar, precisamente).
  • PresentacionMessageBox: Genera la presentación sobre un control visual (MessageBox [11])
  • PresentacionArchivo: Genera la presentación sobre un archivo de texto plano.
Los dos primeros métodos son static, y el tercero, de instancia. Con esto demostramos que los delegados son compatibles con ambas formas de declaración de comportamiento.

Observemos con más cuidado la declaración del método PresentacionArchivo:

En la línea 103 creamos una variable del delegado Presentador y le asignamos null. (Más adelante veremos que este es el modo de inicialización de la pila de referencias a métodos que equivale a tener 0 elementos). En la línea 105, a través del operador compuesto conextual +=, agregamos la primera instancia (referencia) de un método al delegado Presentador, lo mismo ocurre en la siguiente línea (106). En sí, en conjunto, estas líneas nos permite crear un delegado multicast (más adelante trataremos sobre este concepto).

Compilamos:


  1. csc /target:exe Conjunto.cs

Ejecutamos:


  1. .\Conjunto.exe

Resultado:
Archivo de texto plano Salida.txt:

Procesando elemento 1/5: 0
Procesando elemento 2/5: 1
Procesando elemento 3/5: 2
Procesando elemento 4/5: 3
Procesando elemento 5/5: 4

2.1 Acción del compilador y la CLR sobre delegados

Detrás de cámaras, el compilador de C# ya la Common Language Runtime (CLR) llevan a cabo tareas complejas durante la manipulación de delegados. Por ejemplo, para el delegado Presentador el compilador habrá generado una clase como esta (versión simplificada):

public class Presentador : System.MulticastDelegate
{
// Constructor:
public Presentador(Object target, Int32 methodPtr)
{
// Implementación
}

// Método de invocación implícita:
public void virtual Invoke(Object valor, Int32 elemento, Int32 cantidadElementos)
{
// Implementación
}

// Métodos asincrónicos (multicast):
public virtual IAResult BeginInvoke(Object valor, Int32 elemento, Int32 cantidadElementos,
AsyncCallback callback, Object object)
{
// Implementación
}
}

Vemos que el compilador ha creado una clase que representa nuestro delegado, y que además agregar otros elementos constituyentes para permitir la invocación implícita de los métodos que tiene encargados el delegado, evidentemente, cuando creamos un objeto de él. La herencia desde System.MulticastDelegate [12]. Esta herencia permite la conversión de los delegados en una pieza eficiente y efectiva para la manipulación de referencias de métodos [2].

3. Uso de Delegados

Ya en las dos seccionees anteriores (1, y 2) aprendimos acerca de la declaración y uso de delegados. Ahora echemos un vistazo más a fondo sobre el uso de los delegados.

 Podemos declarar un delegado como este:

public delegate void Delegado(string mensaje);

Ahora podemos definir un método compatible con el delegado, es decir, que posea una firma con las siguientes requerimientos:
  • Tipo de retorno void, y 
  • Y un parámetro de tipo string.
Aquí tenemos un método que cumple con los requerimientos anteriores:

// Definición de método compatible con el delegado `Delegado`:
public static void Metodo(string mensaje)
{
Console.WriteLine(mensaje);
}

¿Cómo utilizamos estos dos artefactos? Desde código cliente podemos utilizar los objetos delegados Delegado de la siguiente manera:

// Creación del delegado:
Delegado del = Metodo;

// Invocación (indirecta) por medio del objeto `del`:
del("Blog xCSw");

3.1 Delegados como argumentos de métodos

Gracias a que los delegados son objetos que referencian métodos, es posible especificar en la lista de parámetros de un método, uno o varios parámetros como un tipo de delegado. Veamos este ejemplo:

public void MetodoConDelegado(int param1, int param2, Delegado del)
{
del(String.Format("Suma: {0}", (param1 + param2).ToString() );
}

Este método puede ser invocado de la siguiente manera:

MetodoConDelegado(2, 3, del);

> Prueba de ejecución.

Observemos que el pase del argumento del nos ayuda a independizar el método MetodoConDelegado de cualquier implementación de presentación de contenido (como en ejemplo de la sección 2). Esto nos asegura otro nivel de abstracción sobre el diseño de algoritmos, implementaciones.

3.2 Multicasting

Una instancia de un tipo de delegado puede contener uno o más métodos que cumplan con la especificación del delegado. A esta capacidad se le conoce como multicasting. Es necesario usar el operador compuesto contextual de suma +=.

En el siguiente ejemplo se demuestra la creación de un delegado que soporta la siguiente firma para métodos de instancia y métodos estáticos:

public delegate void Delegado(int param);
  • Retorno: void, y
  • Paráramentros: int
Archivo EjemploMulticasting.cs:

La parte sobresaliente de este ejemplo de multicasting ocurre en las líneas 31 a 37:

  • Líneas 31 a 33: Creación de tres instancias de Delegado (i.e., del1del2, y del3).
  • Línea 36: Aquí creamos una instancia de Delegado que es la suma (me refiero a la suma contextual de instancias de delegados: operador + sobrecargado) de dos delegados: del1 y del2.
  • Línea 37: Adición (agregación a la pila implícita para delegados) de otro delegado: del3, con el operador compuesto contextual de suma +=.

Resultado:
Uso de delegados con multicasting
Figura 1. Uso de delegados con multicasting.
Observemos en la salida de la Figura 1 que, los métodos son invocados de forma secuencial, en este caso, en el orden (inverso) que fueron agregados en la pila.

Es importante mencionar que el operador contextual compuesto de resta, -=, permite remover una referencia a un método desde el delegado:

conjuntoMetodos -= del3;

En conjunto, los operadores +=, y -=, nos entrega un poder de flexibilidad de dinamismo en tiempo de ejecución, permitiéndonos asignar métodos a una estructura abstracta de descripción (los delegados) de métodos. Veremos en una próxima entrega la utilidad de multicasting en el uso de eventos (en respuesta a la actividad generada por el usuario sobre una interfaz gráfica).

3.2.1 Proceso largo

A través del siguiente ejemplo, simularemos un proceso que toma largo tiempo a través del método estático Sleep de la clase Thread:
Archivo ProcesoLargoMulticast.cs:
En la línea 9 se declara el delegado ReporteProgreso que especifica como tipo de retorno void y un parámetro int. Este parámetro hace alusión a un valor entero que comprende entre 0 y 100, es decir, el avance porcentual del proceso largo.

Para simular el efecto de proceso largo, en la línea 20 se utiliza el método estático Sleep de la clase Thread para hacer un retardo simulado de 100 ms. Previamente, en la línea 19, se invoca de forma indirecta el (o los) método(s) asociados con el delegado ReporteProgreso.

En código cliente, líneas 27-33, se crea una instancia del delegado ReporteProgreso para asociar dos métodos compatiblesReporteProgresoConsola, y ReporteProgresoArchivo. El primero corresponde con la generación del reporte (avance porcentual) sobre la consola, y el segundo, sobre un archivo de texto.

Compilación:


  1. csc /target:exe ProcesoLargoMulticast.cs

Resultado:

4. Sobrecarga

Exploremos con brevedad las características claves de los métodos sobrecargados para contrastarlos con los delegados.

Empecemos por ver un ejemplo un ejemplo concreto de un método sobrecargado:

public int Sumar(int a, int b)
{
return a + b;
}

public long Sumar(long a, long b)
{
return a + b;
}

public double Sumar(double a, double b)
{
return a + b;
}

Bien sabemos que las diferencias de las versiones de un método sobrecargado radica en el número y tipo de los parámetros, sin embargo, el tipo de retorno no juega ningún papel crítico en la diferenciación de cada juego de versiones. Esto se debe a que una vez se llama a un método, el valor del tipo de retorno puede ser omitido, es decir, es opcional.

[Nota: Para saber más acerca de métodos sobrecargados ir a Sobrecarga y Llamada Métodos en C#.]

Ahora, en el contexto de los delegados, la firma de un método incluye el tipo de valor de retorno. De no ser así, se generará el error CS0123 [14], cual advierte sobre la incompatibilidad entre las firmas del método y el delegado. Para solucionar este problema, basta con ajustar la firma de una de las dos construcciones.


Resultado:

error CS0123: A method or delegate `Articulos.Cap03.DelegadosFirmaMetodos.Sumar(int, int)' parameters do not match delegate `Articulos.Cap03.DelegadosFirmaMetodos.Operacion(double, double)' parameters

5. Covarianza y Contra-varianza

5.1 Covarianza

La covarianza permite flexibilizar el tipo de retorno, es decir, podemos hacer que se sigan los principios de una jerarquía de herencia. Ejemplifico para que quede más claro:

Creemos esta jerarquía de herencia:
Jerarquía de herencia Mamífero-Perro
Figura 1. Jerarquía de herencia Mamífero-Perro.
Ahora codificamos en código fuente C#:


Echemos un vistazo a la declaración de la firma del delegado:

  • Tipo retorno: Mamifero (clase padre de la jerarquía de la Figura 1).
  • Parámetros: «Ninguno»
Por otro lado, en la línea 15 se crea una variable de tipo Delegado y se asigna un método que retorna el subtipo Perro de la jerarquía de herencia de la Figura 1.

5.2 Contra-varianza

La contra-varianza permite crear métodos parametrizados con los tipos de una jerarquía de herencia o implementación, de tal manera que se puedan escribir métodos con firmas de un nivel de abstracción alto.

Lo anterior, lo podemos explicar con la jerarquía de herencia de eventos de controles visuales: El evento Button.MouseClick genera los argumentos de evento MouseEventArgs  [17] (éste hereda de EventArgs [19]), por otro lado, el evento TextBox.KeyDown genera los argumentos de evento KeyEventArgs [18].

Comprendamos el caso anterior, a través del siguiente ejemplo:

private void MultiManejadorEventos(object sender, EventArgs e)
{
labelResultado.Text = DateTime.Now.ToString();
}

public VentanaPrincipal()
{
InitializeComponent();

// KeyDown genera los argumentos de evento KeyEventArgs,
// el cual es compatible con el segundo parámetro de
// `MultiManejadorEventos`:
       this.btnCalcular.KeyDown += this.MultiManejadorEventos;

// KeyDown genera los argumentos de evento MouseEventArgs,
// el cual es compatible con el segundo parámetro de
// `MultiManejadorEventos`:
this.btnCalcular.MouseClick += this.MultiManejadorEventos;
}

6. Métodos Plug-in

A los métodos que aceptan instancias de delegados para sus parámetros se les conoce como métodos plug-in [1]. En el ejemplo de la sección 2, el método ProcesarElementos. Ahora vamos a ver porqué a través de otro ejemplo.

Creamos un clase utilitaria (y en seguida el código de cliente):


El método Transformar (líneas 10-16) está definido así:

void Transformar(int[] valores, Transformador t)

Descripción:
  • Retorno: void
  • Parámetros: int[]Transformador
De los parámetros de este método el que más nos interesa es el segundo, Transformador, el cual corresponde con un delegado. En el código cliente (líneas 21-44) utilizamos los métodos Cuadrado y Cubo para transformar los valores con las versiones cuadradas y cúbicas de los elementos de datos en los arreglos de enteros de 32 bits.


Resultado: 

Cuadrados para los elementos de `Valores`:
1 4 9 16 25 
Cubo para los elementos de `Valores`:
1 8 27 64 125 

7. Delegados vs Interfaces

Desde uno [1] nos anuncian:
A problem that can be solved with a delegate can also be solved with an interface.
Consultemos el siguiente (el mismo que en la sección 6 pero con el uso de interfaces):

En las líneas 5-8, se declara la interfaz ITransformador esta interfaz posee un método abstracto como parte de su contrato:

int Transformar (int x);


que deber ser implementando por cualquier clase concreta o abstracta que la implemente. En nuestro caso, las clases que lo hacen son Cuadrado (líneas 21-27) y Cubico (29-35).


En código cliente (líneas 39-62) se crean instancias ( new Cuadrado()new Cubico()) como parámetro del método TransformarTodo de la clase Utilitario.


Salida:

Cuadrados para los elementos de `Valores`:
1 4 9 16 25 
Cubo para los elementos de `Valores`:
1 8 27 64 125 

[Nota: Puede comparar los resultados previos con los del ejemplo de la sección 7.]

Sin embargo, en [20] recomiendan el uso de delegados e interfaces (de forma exclusiva) en ciertas circunstancias:
  • Delegados:
    • Nuestra aplicación requiere el uso del patrón de eventos.
    • Se requiere la encapsulación de métodos estáticos.
    • El delegado no necesitará acceder a propiedades, otros métodos, o interfaces sobre el objeto propietario del método referenciado.
    • Una clase que requiera poseer una o más implementaciones del método (sobrecarga).

  • Interfaces:
    • Se requiere la invocación de métodos relacionados (como en el ejemplo del archivo FiltroValoresConInterfaz.cs).
    • Una clase requerirá una sola implementación del método de una interfaz.
    • Las interfaz que requiera la conversión (casting) a otros tipos (clases o interfaces).
Además, en [1] se nombran otras condiciones que deben cumplirse para elegir delegados por encima de interfaces:
  • The interface only a single method.
  • Multicast capability is needed.
  • The subscriber needs to implement the interface multiple time.

8. Conclusiones

En esta primera parte aprendimos acerca de los esenciales de los delegados en C#: su definición (que es muy parecida a la de un método), su uso (en ejemplos muy sencillos). Pasamos por la función primera de los delegados en el lenguaje. Más adelante, a través del ejemplo extendido, Comprendimos que los delegados pueden hacer referencia tanto métodos estáticos como métodos de clase. En la siguiente parte vamos a ver Uso extendido de delegados.

9. Glosario

  • Ad-hoc
  • Callback
  • CLR
  • Compilador
  • Contra-varianza
  • Covarianza
  • Delegado
  • Delegate
  • Filtro
  • Interfaz
  • Método estático
  • Método de instancia
  • Plug-in
  • Tipo

10. Literatura & Enlaces

[1]: C# 5.0 in a Nutshell by Joseph Albahari and Ben Albahari. Copyright 2012 Joseph Albahari and Ben Albahari, 978-1-449-32010-2.
[2]: .NET: An Introduction to Delegates - http://msdn.microsoft.com/en-us/magazine/cc301810.aspx
[3]: Lesson 14: Introduction to Delegates and Events - http://www.csharp-station.com/Tutorials/Lesson14.aspx
[4]: A Beginner's Guide to Delegates - CodeProject - http://www.codeproject.com/Articles/13607/A-Beginner-s-Guide-to-Delegates
[5]: Understanding Delegates in C# - CodeProject - http://www.codeproject.com/Articles/11657/Understanding-Delegates-in-C
[6]: Introduction to Delegate - http://www.dotnetheaven.com/article/introduction-to-delegate
[7]: delegate (C# Reference) - http://msdn.microsoft.com/en-us/library/900fyy8e.aspx
[8]: Modificadores de Acceso en C# | OrtizOL - Experiencias Construcción Software (xCSw) - http://ortizol.blogspot.com/2014/04/modificadores-de-acceso-en-c.html
[9]: new (C# Reference) - http://msdn.microsoft.com/en-us/library/51y09td4.aspx
[10]: Tipos Genéricos en C# - Parte 8: Delegados Genéricos - http://ortizol.blogspot.com/2014/05/tipos-genericos-en-c-parte-8-delegados.html
[11]: MessageBox Class (System.Windows.Forms) - http://msdn.microsoft.com/en-us/library/system.windows.forms.messagebox(v=vs.110).aspx
[12]: MulticastDelegate Class - http://msdn.microsoft.com/en-us/library/system.multicastdelegate(v=vs.110).aspx
[13]: Sobrecarga y Resolución Llamada Métodos - http://ortizol.blogspot.com/2014/02/sobrecarga-y-resolucion-llamada-metodos.html
[14]: Compiler Error CS0123 - http://msdn.microsoft.com/en-us/library/5d8fk6ew(v=vs.90).aspx
[15]: Delegate Class (System) - http://msdn.microsoft.com/en-us/library/system.delegate.aspx
[16]: Compiler Error CS0644 - http://msdn.microsoft.com/en-us/library/hxds244y(v=vs.90).aspx
[17]: MouseEventArgs Class (System.Windows.Forms) - http://msdn.microsoft.com/en-us/library/system.windows.forms.mouseeventargs(v=vs.110).aspx
[18]: KeyEventArgs Class (System.Windows.Forms) - http://msdn.microsoft.com/en-us/library/system.windows.forms.keyeventargs(v=vs.110).aspx
[19]: EventArgs Class (System) - http://msdn.microsoft.com/en-us/library/system.eventargs(v=vs.110).aspx
[20] When to Use Delegates Instead of Interfaces (C# Programming Guide) - http://msdn.microsoft.com/en-us/library/vstudio/ms173173(v=vs.100).aspx


J

No hay comentarios:

Publicar un comentario

Envíe sus comentarios, dudas, sugerencias, críticas. Gracias.