Delegados y expresiones lambda

Un delegado define un tipo que representa referencias a métodos con una lista de parámetros determinada y un tipo de valor devuelto. Se puede asignar un método (estático o de instancia) cuya lista de parámetros y tipo de valor devuelto coincidan a una variable de ese tipo y luego llamarlo directamente (con los argumentos adecuados) o pasarlo como argumento a otro método y después llamarlo. El siguiente ejemplo muestra el uso de delegados.

using System;
using System.Linq;

public class Program
{
    public delegate string Reverse(string s);

    static string ReverseString(string s)
    {
        return new string(s.Reverse().ToArray());
    }

    static void Main(string[] args)
    {
        Reverse rev = ReverseString;

        Console.WriteLine(rev("a string"));
    }
}
  • La línea public delegate string Reverse(string s); crea un tipo delegado de un método que toma un parámetro de cadena y luego devuelve un parámetro de cadena.
  • El método static string ReverseString(string s), que tiene exactamente la misma lista de parámetros y tipo delegado que el definido, implementa el delegado.
  • En la línea Reverse rev = ReverseString; se muestra que se puede asignar un método a una variable del tipo de delegado correspondiente.
  • En la línea Console.WriteLine(rev("a string")); se muestra cómo se usa una variable de un tipo de delegado para invocar al delegado.

Para simplificar el proceso de desarrollo, .NET incluye un conjunto de tipos delegados que los programadores pueden volver a usar para no tener que crear nuevos tipos. Estos tipos son Func<>, Action<> y Predicate<>, y se pueden usar sin necesidad de definir nuevos tipos de delegado. Hay algunas diferencias entre los tres tipos que tienen que ver con la forma en que se van a usar:

  • Action<> se usa cuando es necesario realizar una acción mediante los argumentos del delegado. El método que encapsula no devuelve ningún valor.
  • Func<> se usa normalmente cuando se tiene una transformación a mano; es decir, cuando se necesita transformar los argumentos del delegado en un resultado diferente. Las proyecciones son un buen ejemplo. El método que encapsula devuelve un valor especificado.
  • Predicate<> se usa cuando es necesario determinar si el argumento cumple la condición del delegado. También se puede escribir como Func<T, bool>, lo que significa que el método devuelve un valor booleano.

Ahora podemos tomar el ejemplo anterior y volver a escribirlo mediante el delegado Func<> en lugar de un tipo personalizado. El programa seguirá ejecutándose de la misma forma.

using System;
using System.Linq;

public class Program
{
    static string ReverseString(string s)
    {
        return new string(s.Reverse().ToArray());
    }

    static void Main(string[] args)
    {
        Func<string, string> rev = ReverseString;

        Console.WriteLine(rev("a string"));
    }
}

En este ejemplo sencillo, tener un método definido fuera del método Main parece un poco superfluo. .NET Framework 2.0 ha introducido el concepto de delegados anónimos, que permiten crear delegados "insertados" sin necesidad de especificar ningún otro tipo o método.

En el ejemplo siguiente, un delegado anónimo filtra una lista solo por los números pares y, después, los imprime en la consola.

using System;
using System.Collections.Generic;

public class Program
{
    public static void Main(string[] args)
    {
        List<int> list = new List<int>();

        for (int i = 1; i <= 100; i++)
        {
            list.Add(i);
        }

        List<int> result = list.FindAll(
          delegate (int no)
          {
              return (no % 2 == 0);
          }
        );

        foreach (var item in result)
        {
            Console.WriteLine(item);
        }
    }
}

Como puede ver, el cuerpo del delegado es simplemente un conjunto de expresiones, como cualquier otro delegado. Pero en lugar de ser una definición independiente, se ha introducido ad hoc en la llamada al método List<T>.FindAll.

Pero incluso con este enfoque, sigue habiendo mucho código del que es posible deshacerse. Aquí es donde entran en juego las expresiones lambda. Las expresiones lambda, o las "lambda", para abreviar, se presentaron en C# 3.0 como uno de los bloques de compilación fundamentales de Language Integrated Query (LINQ). Constituyen una sintaxis más cómoda para el uso de delegados. Declaran una lista de parámetros y un cuerpo del método, pero no tienen una identidad formal propia, a menos que se asignen a un delegado. A diferencia de los delegados, se pueden asignar directamente como lado izquierdo del registro de eventos, o bien en distintas cláusulas y métodos de LINQ.

Puesto que una expresión lambda es solo otra forma de especificar un delegado, debería ser posible volver a escribir el ejemplo anterior para usar una expresión lambda en lugar de un delegado anónimo.

using System;
using System.Collections.Generic;

public class Program
{
    public static void Main(string[] args)
    {
        List<int> list = new List<int>();

        for (int i = 1; i <= 100; i++)
        {
            list.Add(i);
        }

        List<int> result = list.FindAll(i => i % 2 == 0);

        foreach (var item in result)
        {
            Console.WriteLine(item);
        }
    }
}

En el ejemplo anterior, la expresión lambda que se usa es i => i % 2 == 0. De nuevo, constituyen una sintaxis más cómoda para el uso de delegados. Lo que sucede en segundo plano es similar a lo que ocurre con el delegado anónimo.

De nuevo, las expresiones lambda son solo delegados, lo que significa que se pueden usar como controlador de eventos sin problemas, como se muestra en el siguiente fragmento de código.

public MainWindow()
{
    InitializeComponent();

    Loaded += (o, e) =>
    {
        this.Title = "Loaded";
    };
}

En este contexto, el operador += se usa para suscribirse a un evento. Para obtener más información, vea Procedimiento para suscribir y cancelar la suscripción a eventos.

Más información y recursos