martes, 15 de abril de 2014

Delegados - Delegates - parte 3




La covarianza y la contravarianza son uno de esos conceptos dentro de la programación que a priori, no solemos verles una funcionalidad o un uso demasiado práctico, pero que cuando manejas, te das cuenta que vienen a romper una serie de limitaciones que sin su existencia nos haría tener que decir eso de ‘no se puede hacer’ en más ocasiones al día. 

Esto no es menos importante para LinQ  ya que tienen una aparición estelar con los delegados genéricos Func.

Para finalizar también veremos la posibilidad de realizar ejecuciones asíncronas con nuestros delegados.




Dentro de las mejoras que nos ofreció la versión 4.0 del .NET Framework, estaba la llamada Covarianza y Contravarianza. Estos conceptos están 100% ligados al de Generics. Vamos a definir cada uno de ellos:
  • Covarianza .- Es la capacidad que tiene una sentencia genérica para devolver un tipo derivado de su variable de tipo. Para poder indicar la covarianza es necesario indicar la palabra reservada out antes del parámetro de tipo:

public delegate T DelegadoCovariante<out T>();

  • Contravarianza .- Tiene el mismo concepto básicamente que la covarianza, pero esta se aplica para el parámetro de tipo de los posibles parámetros que pueda contener la sentencia. En este caso la palabra reservada es in.

public delegate void DelegadoContravariante<in T>(T t);

Pues todo esto es algo que podemos llevar perfectamente al uso de nuestros delegados.

Tenemos dos clases, Humano y Hombre:

public class Humano { }
public class Hombre : Humano { }

Definimos nuestro delegado genérico:

public delegate K DelegadoNormal<T, K>(T t);

Como podemos apreciar es un delegado con una firma en la que debe de aceptar métodos en los que hagan uso de un argumento del tipo de su primer parámetro de tipo, en este caso el T y devuelvan un objeto de su segundo parámetro de tipo en este caso K.

Añadimos dos métodos:

private Hombre DevolverHombre(string nombre)
{
    /// ... ... ... ...
            
    return new Hombre ();
}
 
 
private Humano DevolverHumano(string nombre)
{
    /// ... ... ... ...
            
    return new Humano();
}


Realizamos una instanciación de nuestro delegado genérico que almacene cada uno de estos dos métodos:

DelegadoNormal<string, Humano> del1 = DevolverHumano;
DelegadoNormal<string, Hombre> del2 = DevolverHombre;

Si hacemos uso de nuestros conocimientos de POO y las reglas del Polimorfismo pensaríamos que hacer esta asignación debería de ser acertada ya que un hombre siempre va a ser un humano:

del1 = del2;

Pero no es así, como podremos apreciar en la imagen inferior, el compilador no nos deja hacer esto y nos indica que no se puede realizar el casting:






La forma de permitir esto es aplicando covarianza a nuestra definición y añadiendo el out en la definición de delegado:

public delegate K DelegadoNormal<T, out K>(T t);

Para el caso de la contravarianza pasaría exactamente lo mismo.


Añadimos otros dos métodos muy similares a los anteriores:

private Humano DevolverHumanoString(string s)
{
    /// ... ... ... ...
            
    return new Humano(); 
}
 
 
private Humano DevolverHumanoObject(object o)
{
    /// ... ... ... ...
            
    return new Humano();
}

Realizamos las asignaciones del delegado:

DelegadoNormal<object, Humano> del1 = DevolverHumanoObject;
DelegadoNormal<string, Humano> del2 = DevolverHumanoString;

Realizamos la asignación y nos da el mismo error que el anterior:

del2 = del1;

Para arreglar esto aplicamos la contravarianza (in) en nuestra definición del delegado:

public delegate K DelegadoNormal<in T, K>(T t);



Ejecución asíncrona con delegados

Los delegados ofrecen la posibilidad de poder ejecutar el método o los métodos almacenados de manera asíncrona, ósea en un thread diferente al de la ejecución principal.


En esta parte entran en juego 3 actores fundamentales:
  1. La interfaz IAsynResult .- Que representa el estado de una ejecución asíncrona.
  2. El método BeginInvoke .- Inicia la ejecución asíncrona.
  3. El método EndInvoke .- Recupera los resultados de la llamada asíncrona.
Vamos a ver un ejemplo:

De primeras añadiremos el típico ejemplo de método que comprueba si un número es o no primo. Esto lo utilizaremos para retardar un poco la ejecución.

public static bool EsPrimo(int candidato)
{
    if (candidato % 2 <= 0)
        return candidato == 2;
 
    int power2 = 9;
 
    for (int divisor = 3; power2 <= candidato; divisor += 2)
    {
        if (candidato % divisor == 0)
            return false;
        power2 += divisor * 4 + 4;
    }
    return true;
}
 
public static int NumerosPrimos(int maxNumber)
{
    int resultado = 0;
 
    for(int i = 0; i <= maxNumber; i++)
        if (EsPrimo(i))
            resultado++;
 
    return resultado;
}

Después de esto añadiremos el método de ejecución principal:

public delegate int MiDelegado(int num);
 
static void Main(string[] args)
{
    MiDelegado miDelegado = new MiDelegado(NumerosPrimos);
    // Inciamos la ejecución asíncrona
    IAsyncResult resultadoEjecucion = miDelegado.BeginInvoke(10000000, EjecucionCompletada, null);
            
    Console.WriteLine("Finalizado !!! a las {0}", DateTime.Now);
    Console.Read();
}
 
private static void EjecucionCompletada(IAsyncResult resultado)
{
    // Casteamos la Interface a la Clase
    AsyncResult asincResult = (AsyncResult) resultado;
    // Casteamos el delegado
    MiDelegado md = (MiDelegado)asincResult.AsyncDelegate;
    // Consultamos en resultado
    int resultadoEjec = md.EndInvoke(resultado);
 
    Console.WriteLine("Se han encontrado {0} números primos a las {1}", resultadoEjec, DateTime.Now);
}


Como podemos ver tenemos primero la definición de nuestro delegado, que admite métodos que devuelvan un int y reciban un parámetro int, algo que concuerda a la perfección con nuestro método NumerosPrimos.


En nuestro método Main, Instanciamos nuestro delegado y en su constructor le facilitamos el método que le servirá como ejecución principal. Después llamamos a BeginInvoke para iniciar la ejecución asíncrona, indicando 3 parámetros:

  1. Este pude ser uno o n parámetros de seguido que se corresponderán con el número y tipo de parámetros que necesita el método principal, en nuestro caso un int.
  2. Una referencia al método que se ejecutará cuando el método principal finalice.
  3. Será el que guarde la información al estado de ejecución del método principal, nosotros no lo utilizamos.

Para finalizar tenemos el método que se ejecuta cuando se finaliza la ejecución del método principal EjecucionCompletada. En este método nos aprovechamos de su parámetro de tipo IAsyncResult para obtener la información del resultado del método principal y en nuestro ejemplo pintarla.

El resultado de la ejecución sería la siguiente:











Como podemos ver en las horas de ejecución, ejecuta primero el método Main completo y después el método de EjecucionCompletada, por lo que se puede comprobar que se han ejecutado en threads diferentes.



Con esto damos por finalizado el tema de delegados para adentrarnos de lleno en los eventos.