DEV Community

Cover image for Píldoras de C#: Delegates, Métodos Anónimos, Expresiones Lambda y Eventos
Eduardo Barrios
Eduardo Barrios

Posted on

Píldoras de C#: Delegates, Métodos Anónimos, Expresiones Lambda y Eventos

En alguna ocasión te has preguntado como almacenar un método en un tipo de dato de C#, o te has preguntado como pasar un método como parámetro a otro método y como carajos funcionan los eventos. Estás características lenguajes de programación como JavaScript lo hacen a menudo, por su parte C# puede lograr todo esto mediante Delegates. En este Post abordaremos los siguientes puntos sobre delegates en C#.

  • ¿Qué es un Delegate, como creo un Delegate?
  • Delegates integrados de .NET
  • Métodos Anónimos
  • Expresiones Lambda
  • Eventos
  • Utilizando Delegates (Xamarin Forms, ASP.NET Core WebApi)

Qué es un Delegate

Un delegate en C# es un tipo de dato como lo es una estructura, o una clase, estos tipos definen propiedades, métodos y algunas veces eventos, por su parte los delegates se encargan de referenciar métodos que coincidan con una firma compatible determinada por los parámetros de entrada y tipo de retorno. Podemos comparar los delegates con los punteros de C++, un delegate de C# apunta a la dirección en memoria de 1 o más métodos esto gracias a la Multidifusión que permite anidar métodos a una instancia de un tipo delegate, los delegates de C# nos permiten crear instancias de un tipo delegate y dejar que apunte a otro método y así mismo invocar el método a través de la instancia del tipo delegate, esto es muy útil para poder pasar métodos como parámetros hacía otros métodos o para poder crear manejadores de Eventos que no son más que métodos que se invocan mediante delegates.
Imaginemos los siguientes casos, una aplicación de escritorio desarrollada con Windows Form poseé una interfaz gráfica con elementos Button, Combobox, TextBox, etc, el usuario puede realizar acciones presionando click sobre un Button o cualquiera de los otros elementos visuales, la acción de realizar un click desencadena un Evento, aquí entran los delegates ya que podemos manejar ese evento y hacer múltiples cosas mediante un manejador de eventos que se suscriba al Evento lanzado, lo importante en este aspecto es que fue un delegate el que invoco el Evento.
Veamos el caso de una Aplicación Móvil desarrollada con Xamarin Forms, supongamos que tenemos un Content Page que internamente contiene un ListView con objetos genéricos, la acción de realizar un Tap sobre cualquier item o Swipe desencadenará un Evento y es aquí donde otra vez vemos delegates en acción.
C# es un lenguaje de programación fuertemente tipado y muchos de los tipos contenidos dentro de los Assemblies de DotNet funcionan entorno a delegates.
Los delegates de C# podemos crearlos personalizados es decir crear nuestros propios tipos definiendo una firma especifica o podemos reutilizar los propios delegates de DotNet que veremos más adelante.

Veamos como crear un tipo delegate, la siguiente sintaxis define su estructura

[identificador de acceso] delegate [Tipo de retorno] [Nombre del Delegate]([Parámetros de entrada]); 
Enter fullscreen mode Exit fullscreen mode

Con base en la sintaxis anterior crearemos un delegate que apunte a métodos con coincidan con una firma determinada.

public delegate int CalculateDelegate(int x, int y);
Enter fullscreen mode Exit fullscreen mode

Mejores prácticas: Algunos programadores agregan la palabra Delegate al final del nombre del tipo Delegate. Esto es muy común pero no es universal tampoco obligatorio.

Ahora necesitamos métodos para almacenar en nuestro tipo Delegate y hacer uso del tipo Delegate para referenciar esos métodos.

public int Add(int x, int y)
{
   return x + y;
}

public int Multiply(int x, int y)
{
   return x * y;
}

public void TestDelegate()
{
   CalculateDelegate instanceDelegate= Add;
   Console.WriteLine(instanceDelegate(5, 4)); // Salida 9

   instanceDelegate = Multiply;
   Console.WriteLine(instanceDelegate(5, 4)); // Salida 20
}
Enter fullscreen mode Exit fullscreen mode

Hemos creado un tipo Delegate muy básico y tenemos una instancia que podríamos utilizar para presentárselo a un método como argumento. Como podemos observar el código anterior solo instanciamos una vez el tipo Delegate y la segunda vez asignamos un segundo método sin nada más, esto gracias a que desde C# 2.0 se agregó la creación automática de un nuevo delegate cuando se asigna un grupo de métodos a un tipo delegate.

Otra característica importante de los Delegates de C# es la multidifusión que permite combinar y anidar métodos a la lista de invocación de una instancia de delegado existente, utilizamos el operador + o += para lograrlo.

public void MethodOne()
{
   Console.WriteLine("Método 1");
} 

public void MethodTwo()
{
   Console.WriteLine("Método 2");
} 

// Definir el Tipo Delegate
public delegate void FunctionDelegate();

public void TestMulticast()
{
   FunctionDelegate d = MethodOne;
   d += MethodTwo;

   d();
}

// Salida:
// Método 1
// Método 2
Enter fullscreen mode Exit fullscreen mode

Nota: La multidifusión de los Delegates es la característica que permite suscribirse a Eventos y manejadores de Eventos.

Delegates integrados de .NET

.NET define dos tipos de delegates genéricos(Action y Func) ambos los podemos utilizar para evitar definir nuestros propios delegates.

Delegate Action

El Delegate genérico Action representa un método que devuelve void. Diferentes versiones de Action toman entre 0 y 16 parámetros de entrada, a menos que necesitemos un delegate que tome más de 16 parámetros de entrada y devolver void se recomienda utilizar Action.
Veamos el ejemplo anterior de los métodos Add y Multiply que toman dos tipos int como parámetros de entrada y en este caso devuelven void. Cambiaremos la firma de ambos métodos.

public void Add(int x, int y)
{
   Console.WriteLine(x + y);
}

public void Multiply(int x, int y)
{
   Console.WriteLine(x * y);
}

public void TestDelegate()
{
   Action<int, int> action = Add;
   action(5, 4); // Salida 9

   action = Multiply;
   action(5, 4); // Salida 20
}
Enter fullscreen mode Exit fullscreen mode

Nota: Action en su declaración toma dos tipos int, ambos hacen referencia a los dos parámetros de entrada tipo int que son requeridos y deben coincidir con la firma del método y el delegate.

Delegate Func

El Delegate Func es otro de los tipos integrados de .NET que representa un método que devuelve un valor, como el caso de Action, Func tiene múltiples versiones que toman entre 0 y 16 parámetros de entrada y a menos que no sea necesario presentar más de 16 parámetros de entrada y devolver un valor se recomienda utilizar el tipo delegate Func. Veamos el ejemplo anterior de los métodos Add y Multiply que toman 2 parámetros de entrada tipo int y devuelven un int como resultado, para esto regresaremos a la firma de ambos métodos como fueron definidos en un principio.

public int Add(int x, int y)
{
   return x + y;
}

public int Multiply(int x, int y)
{
   return x * y;
}

public void TestDelegate()
{
   Func<int, int, int> func = Add;
   Console.WriteLine(func(5, 4)); // Salida 9

   func = Multiply;
   Console.WriteLine(func(5, 4)); // Salida 20
}
Enter fullscreen mode Exit fullscreen mode

Nota: Func en su declaración toma 3 tipos int, los primeros dos tipos int representan la cantidad de parámetros de entrada, en este caso son 2 valores de tipo int, el tercer int en la declaración del delegate Func hace referencia al tipo a devolver, el resultado de la operación será un int y la firma de ambos métodos especifican que deben devolver un tipo int.

Delegate Predicate

Predicate es otro Delegate integrado de .NET como lo son Action y Func. Predicate está definido por la siguiente firma.

public delegate bool Predicate<in T>(T obj);
Enter fullscreen mode Exit fullscreen mode

Predicate representa un método que contiene un conjunto de criterios a verificar y valida si el parámetro pasado cumple con esos criterios. Es obligatorio para Predicate pasar un parámetro de entrada y siempre devuelve un booleano true o false.
Veamos un ejemplo para entender como funciona Predicate.
Supongamos que necesitamos validar cuentas de correo electrónico, tenemos una lista de cadenas que contiene múltiples cuentas de correo electrónico, pero entre la lista existen cuentas de correo electrónico no validas, con base en esa primer lista necesitamos crear una segunda lista que contenga solo las cuentas de correo electrónico válidas. Crearemos un Delegate Predicate para lograr este objetivo.

// Método que pasaremos al Delegate Predicate
// Este método devolverá true o false dependiendo si el email es válido
public bool EmailIsValid(string email)
{
   string pattern = @"^([\w-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$";
   return Regex.IsMatch(email, pattern);
}

public void TestDelegate()
{
   // Lista de emails
   var emails = new List<string>()
           {
                "admin@gmail.com",
                "admin@gmail",
                "2gmail.com",
                "test@hotmail.com"
            };

   // Creamos el Predicate
   Predicate<string> predicate = new Predicate<string> (EmailIsValid);

   // Utilizamos el Predicate para crear una nueva lista con emails válidos
   var validEmails = emails.FindAll(predicate);

   // Recorremos la nueva lista
   foreach (var email in validEmails)
   {
      Console.WriteLine(email);
   }
}

// El resultado es:
// admin@gmail.com
// test@hotmail.com
Enter fullscreen mode Exit fullscreen mode

Métodos Anónimos

Los métodos anónimos no son más que métodos sin nombre. En lugar de crear métodos como normalmente lo hacemos creamos un delegado que haga referencia al código que debería contener el método tradicional internamente, con esto podremos utilizar ese delegado como si fuera una variable de delegado que contiene una referencia al método. A continuación la sintaxis correcta para crear un método anónimo.

delegate([parámetros de entrada]){ Código; }
Enter fullscreen mode Exit fullscreen mode

Modifiquemos el caso anterior pero ahora en lugar de pasar como argumento el método tradicional que cumple con la firma de un Predicate, pasaremos la definición del método como argumento y como un método anónimo.

public void TestPredicate()
{
   Predicate<string> predicate = new Predicate<string>(
                     delegate(string email)
                     {
                       string pattern = @"^([\w-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$";
                       return Regex.IsMatch(email, pattern);
                      });

   var validEmails = emails.FindAll(predicate);

   foreach (var item in validEmails)
   {
     Console.WriteLine(item);
   }
}
Enter fullscreen mode Exit fullscreen mode

Expresiones Lambda

Suelen suceder casos en los que la firma completa de un método puede ser más código que el propio cuerpo del método, también hay situaciones en las que necesitamos crear un método completo solo para usarlo en un delegado. Para esos casos Microsoft agregó algunas características importantes y útiles en C#. En C# 2.0 se agregaron los métodos anónimos, en C# 3.0 mejoraron aún más cuando se agregaron las expresiones lambda. Las expresiones lambda nos permiten utilizar una sintaxis concisa y más corta para que de otra forma podamos escribir métodos anónimos.
A continuación la sintaxis correcta para escribir una expresión lambda.

() => expression;
Enter fullscreen mode Exit fullscreen mode

Nota: Los paréntesis vacíos representan la lista de parámetros vacía tomada por el método anónimo, => indica que se trata de una expresión lambda, en este caso la expresión lambda es de una sola instrucción, si el método necesita más de una instrucción se deben agregar {} de la siguiente manera.

() => 
{
  // Instrucción 1;
  // Instrucción 2;
}
Enter fullscreen mode Exit fullscreen mode

Nota Importante: Pasar parámetros de entrada a un método anónimo con sintaxis de expresión lambda es sumamente sencillo, debemos agregar los parámetros dentro de los paréntesis de la expresión, cuando es un solo parámetro podemos omitir los paréntesis y escribir únicamente el nombre del único parámetro. Otro punto importante es el tipo de los parámetros podemos agregarlos o dejar que la característica de inferencia de tipos de C# haga el trabajo.

// Pasando un solo parámetro
Action<string> action1 = (nombre) => Console.WriteLine($"Hola {nombre}");

// Pasando un solo parámetro sin los paréntesis
Action<string> action2 = nombre => Console.WriteLine($"Hola {nombre}");

// Pasando dos parámetros sin especificar el tipo
Action<int, int> action3 = (a, b) => Console.WriteLine(a + b);

// Pasando dos parámetros especificando el tipo
Action<int, int> action4 = (int a, int b) => Console.WriteLine(a + b);
Enter fullscreen mode Exit fullscreen mode

Mejores Prácticas: Incluir los paréntesis aunque solo necesitemos un parámetro de entrada para una expresión lambda es una buena práctica ya que hace más legible el código. Naturalmente las expresiones lambda ya son bastante confusas por lo que muchos desarrolladores las incluyen aunque no sean necesarias.

Eventos

Los eventos permiten que exista una comunicación entre objetos y un programa para indicar cuando algo interesante ha sucedido o se ha bloqueado, por ejemplo un objeto de correo electrónico podría generar un evento para indicar al programa que ha recibido un mensaje nuevo.
Una solución reutilizable para problemas recurrentes con base en eventos en el desarrollo de aplicaciones es el de Editor-Suscriptor, consiste en suscribirse a un evento y luego notificar cuando el editor del evento plantea un nuevo evento, naturalmente esto se usa para establecer un acoplamiento flojo en componentes de una aplicación.
A continuación un ejemplo sencillo del patrón Editor-Suscriptor y Eventos.

public class Editor
{
   public event Action OnChange = delegate { };

   public void Raise()
   {
     OnChange();
   }
}

public void TestEvents()
{
  Editor editor = new Editor();
  editor.OnChange += () => Console.WriteLine("Evento generado en el método 1");
  editor.OnChange += () => Console.WriteLine("Evento generado en el método 2");

  editor.Raise();
}
Enter fullscreen mode Exit fullscreen mode

Utilizando Delegates

Xamarin Forms

Pensemos en el siguiente escenario. Tenemos una aplicación móvil desarrollada con Xamarin Forms que contiene un control Entry que recibe el nombre del usuario y existe un control Button que al presionarlo muestra un Dialogo que saluda al usuario, pensando como desarrollador Xamarin lo resolvemos haciendo un Binding a la propiedad Text del Entry y otro Binding a la propiedad Command del control Button, una vez que tenemos capturado el valor del Entry creamos el Command que se ejecutara cuando el usuario presione el Button y nuestro Command quedaria de la siguiente manera.

private string name;
public string Name
{
   get { return name; }
   set { name = value; }
}

public ICommand ExecuteCommand
{
   get
   {
     return new Command(ExecuteMethod);
   }
}

private void ExecuteMethod()
{
   Application.Current.MainPage.DisplayAlert("Delegates App", $"Hola: {this.Name}", "Ok");
}
Enter fullscreen mode Exit fullscreen mode

Está es la manera más habitual para resolver el problema planteado, pero observemos la nueva instancia de Command, Command según la documentación oficial de Microsoft todas las sobrecargas de constructores reciben delegates como parámetros los ya conocidos anteriormente tipos integrados por .NET Action y Func. Lo que estamos haciendo es utilizar delegates y tal vez ves no lo sabias.

Alt Text

Podemos variar la implementación tradicional pero antes analizar si en lugar de escribir un método completo con una firma más extensa que el propio cuerpo del método o si el delegado que necesita nuestro Command solo se utilizará una vez podemos pasar como parámetro al Command un método anónimo como tal o con sintaxis de expresión lambda como lo haremos a continuación.

private string name;
public string Name
{
   get { return name; }
   set { name = value; }
}

public ICommand ExecuteCommand
{
   get
   {
     // Pasando el delegate como método anónimo a la nueva instancia de Command
     return new Command(delegate() { Application.Current.MainPage.DisplayAlert("Delegates App", $"Hola: {this.Name}", "Ok"); });
   }
}
Enter fullscreen mode Exit fullscreen mode
private string name;
public string Name
{
   get { return name; }
   set { name = value; }
}

public ICommand ExecuteCommand
{
   get
   {
     // Pasando el delegate como método anónimo con sintaxis lambda a la nueva instancia de Command
     return new Command(() => { Application.Current.MainPage.DisplayAlert("Delegates App", $"Hola: {this.Name}", "Ok"); });
   }
}
Enter fullscreen mode Exit fullscreen mode

ASP.NET Core WebApi

Pensemos en el siguiente escenario. Tenemos un WebApi desarrollado con DotNet Core con una implementación genérica del patrón Repository y UnitOfWork. Esta implementación cuenta con un método Get que devuelve una lista genérica de objetos después de heredar implementaciones en una clase abstracta y no recibe ningún parámetro de entrada. El método Get de momento solo devuelve una lista mediante un contexto de Entity Framework, el nuevo requerimiento es que debemos poder agregar condiciones Where pasadas como argumento y como expresión lambda al método Get para poder filtrar los objetos por condiciones genéricas, otro requerimiento es poder ordenar esa lista pasando una expresión lambda como argumento al método Get y por último agregar Includes para obtener información adicional con objetos relacionados mediante sus propiedades de navegación.
Necesitamos pasar expresiones lambda como argumentos al método Get, bueno pensemos en delegates, realmente podría escribir un tipo delegate personalizado, pero considero que no es necesario y podría aprovechar la existencia de los tipos integrados de .Net. Con base en los requerimientos necesitamos devolver un valor y necesitamos pasar parámetros pensemos en resolver la problemática con el Delegate Func, la implementación con los nuevos requermientos quedaría de la siguiente manera.

public interface IGenericRepository<T> where T : class
{
   IEnumerable<T> Get(Expression<Func<T, bool>> whereCondition = null, 
                      Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null, 
                      string includeProperties = string.Empty);
}

public abstract class GenericRepository<T> : IGenericRepository<T> where T : class
{
   private readonly IUnitOfWork _unitOfWork;

   public GenericRepository(IUnitOfWork unitOfWork)
   {
      _unitOfWork = unitOfWork;
   }

   public IEnumerable<T> Get(Expression<Func<T, bool>> whereCondition = null,
                         Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null,
                         string includeProperties = string.Empty)
   {
      IQueryable<T> query = _unitOfWork.Context.Set<T>();

      if (whereCondition != null)
      {
         query = query.Where(whereCondition);
      }

      foreach (var includeProperty in includeProperties.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
      {
         query = query.Include(includeProperty);
      }

      if (orderBy != null
         return orderBy(query).ToList();

      else
         return query.ToList();         
   }
}
Enter fullscreen mode Exit fullscreen mode

Hemos agregado el Delegate Func a la firma del Método Get para cumplir los requerimientos, ahora vamos a probar y para eso necesitamos heredar del Repositorio Genérico.

// Modelo Product necesario para una implementación de un CustomRepository
public class Product
{
   public int ProductID { get; set; }
   public string Description { get; set; }
   public double Price { get; set; }
   public DateTime Date { get; set; }
}

public interface IProductRepository: IGenericRepository<Product>
{ }

// Implementación del CustomRepository
public class ProductRepository : GenericRepository<Product>, IProductRepository
{
   public ProductRepository(IUnitOfWork _unitOfWork)
   : base(_unitOfWork)
   { }
}
Enter fullscreen mode Exit fullscreen mode

Con los requerimientos completados podemos recuperar objetos según cualquier condición, ordenar por cualquier propiedad del modelo e incluir relaciones.
Recuperemos los productos que tengan fecha de hoy, ordenados por su precio del más alto hasta el más bajo y sin ninguna relación, el siguiente código realiza esa tarea.

// Prueba del método Get con los nuevos requermientos
public class TestDelegateWebApi
{
   private readonly IProductRepository _customRepository;
   public TestDelegateWebApi(IProductRepository customRepository)
   {
      this._customRepository = customRepository;
   }

   public void GetProducts()
   {
      var products = this._customRepository.Get(product => product.Date == DateTime.Now, product => product.OrderByDescending(p => p.Price), string.Empty);
   }
}
Enter fullscreen mode Exit fullscreen mode

Conclusiones

  • Es importante saber como crear nuestros propios tipos delegados pero también es importante evitarlo y según nuestras necesidades debemos reutilizar los tipos integrados de .NET.

  • Los delegados forman la base del sistema de eventos en C#.

  • Escribir métodos de manera anónima o como expresión lambda es opcional y depende del programador.

  • Los eventos en C# no son propios de un tipo de proyecto de .NET sino del lenguaje C# como tal.

  • Microsoft recomienda que todos los eventos proporcionen dos parámetros: el objeto que genera el evento y otro objeto que proporciona argumentos que son relevantes para el evento. El segundo objeto debe ser de una clase derivada de la Clase EventArgs.

Referencias:

Discussion (0)