Delegados en C#

1 - Qué es un delegado

Podemos indicar que un delegado es una referencia a un método.

Cuando definimos un delegado lo que estamos haciendo es declarar una variable que apunta a otro método. 

Podemos definir un delegado utilizando la palabra clave delegate seguido de la declaración de la función como vemos a continuación:

<modificador de acceso> delegate <tipo de retorno> <nombre>(<parametros>[]);

Lo cual lo podemos traducir al siguiente código:

public delegate void ImprimirDelegado(string value);

1.1 - Ejemplo delegado

Ahora llega la hora del pequeño ejemplo;

Tenemos el siguiente código:

namespace PA.Delegates
{
    public delegate void ImprimirDelegado(string value);
    public class EjemploDelegado
    {
        private void Imprimir(string valor)
        {
            Console.WriteLine(valor);
        }

        public void EjemploDelegate()
        {
            //Declaración
            ImprimirDelegado imprimirDelegado = new ImprimirDelegado(Imprimir);

            //invocación
            imprimirDelegado("texto de ejemplo");
        }

    }
}

Debemos fijarnos en varios puntos :

  • Primero, disponemos fuera de la clase, aunque puede estar dentro el delegado.
  • Posteriormente una clase, que contiene:
    • En método llamado “imprimir” que tiene como tipo de entrada y como tipo de retorno los mismos que el delgado.
    • Método principal el cual declara el delegado, pasando por parámetro el método “imprimir” que acabamos de crear.
      • La invocación al delegado, pasando texto por parámetro. 

Es importante tener en cuenta que el tipo de retorno y los parámetros de entrada al método deben coincidir entre método y delegado, de lo contrario nos dará error de compilación. 

 

2 - Trabajando con delegados y generics

El ejemplo que acabamos de ver es muy sencillo y nos puede servir de introducción, y antes de pasar al grueso, hay que indicar que los delgados también pueden funcionar con generics. 

Si no se tienen claros los conceptos de generics, es conviene revisar este link  sobre generics en C#.

Primero modificamos el valor para que acepte generics

public delegate void ImprimirDelegado<T>(T value);

Y posteriormente hacemos lo propio con el método, en este caso crearemos dos métodos. 

El que ya tenemos, y uno que acepte int como parámetro. 

Y cuando lo declaramos únicamente debemos indicar el tipo que va a contener: 

public class EjemploDelegado
{
    private void Imprimir(string valor)
    {
        Console.WriteLine(valor);
    }

    private void Imprimir(int valor)
    {
        Console.WriteLine($"el valor es {valor}");
    }

    public void EjemploDelegate()
    {
        //Declaración
        ImprimirDelegado<string> imprimirDelegado = new ImprimirDelegado<string>(Imprimir);
        ImprimirDelegado<int> imprimirDelegadoEntero = new ImprimirDelegado<int>(Imprimir);

        //invocación
        imprimirDelegado("texto de ejemplo");
        imprimirDelegadoEntero(25);
    }

}

Dentro de .NET hay varios tipos de delegados ya escritos que se utilizan prácticamente cada día, estos son Func, Action y Predicate

 

3 - Delegado Action<T>

Un delegado Action nos permite que será un delegado que apunta a un método, el cual devuelve void, ya que los Action<T> siempre devuelven void, pero puede contener de 0 a 16 parámetros. Y los tipos de estos parámetros están determinado por los tipos genericos.

Por ejemplo: un Action<string>  es un método que devuelve void pero que necesita recibir un string

Lo cual es exactamente el método Imprimir que ya tenemos implementado con el método Imprimir.

Por lo que podemos asignarle el método al Action<string> y posteriormente invocarlo.

private void Imprimir(string valor)
{
    Console.WriteLine(valor);
}

public void EjemploAction()
{
    Action<string> imprimirAction = Imprimir;
    imprimirAction("ejemplo");
}

 

3.1 - Métodos delegados anónimos

Muy similar a lo que entendemos como tipo anonimo disponemos de los métodos anónimos.

Cuando definimos un Action, podemos asignar directamente el valor del delegado al que hace referencia:

public void EjemploActionAnonimo()
{
    Action<string> imprimirAction = delegate (string valor)
    {
        Console.WriteLine(valor);
    };

    imprimirAction("ejemplo");
}

Este código se puede mejorar con una lambda expression las cuales veremos lo que son más adelante:

Action<string> imprimirAction = v => Console.WriteLine(v);

Lo dicho, lo veremos en detalle más adelante. 

 

4 - Delegado Func<in T, out TResult> 

Similar al caso anterior, acepta de 1 a 16 parámetros y los tipos son genéricos. Con la diferencia de que en este caso, Func<in T, out TResult> debe devolver un valor, y el último tipo que se le asigna a Func es el tipo de retorno. 

En otras palabras un Func<int, string> es una función que recibe un int y devuelve un string:

Func<int, string> resultado = v => $"el resultado es{v}";
Console.WriteLine(resultado(5));

Como hemos indicado acepta múltiples parámetros por ejemplo si realizamos una multiplicación. 

Func<int, int, int> multiplicacion = (v1, v2) => v1 * v2;
int valor = multiplicacion(3, 2);
Console.WriteLine($"El resultado es {valor}");

 

5 - Delegado Predicate<T>

Similar a los dos anteriores, en este caso, un predicate SIEMPRE devuelve un boolean. Por ejemplo Predicate<int> recibe un int y devuelve un booleano.

Predicate<int> mayorDeEdad = e => e >= 18;
bool esMayorDeEdad = mayorDeEdad(10);

 

Conclusión

En este post hemos visto cómo instanciar un delegado y cómo utilizar estos delegados Func, Action y Predicate. Tener claros los conceptos de delegados es extremadamente útil

Utilizaremos delegados en nuestro día a día continuamente, lo cual nos ayudará a tener un código limpio y de mayor calidad. 

Pero no solo eso, sino que cuando utilizamos LINQ estamos continuamente utilizando Func<T>.