Módulo 15: Delegados, eventos y lambdas

Objetivos del módulo

  • Entender qué son los delegados y cómo usarlos
  • Dominar Action, Func y Predicate
  • Crear y suscribirse a eventos
  • Escribir expresiones lambda

1. ¿Qué es un delegado?

📘 Concepto: Un delegado es un tipo que representa una referencia a un método. Es como una variable que almacena una función. Permite pasar funciones como parámetros a otros métodos.

// 1. Definir el tipo delegado (firma del método)
delegate double Operacion(double a, double b);

// 2. Métodos que coinciden con la firma del delegado
static double Sumar(double a, double b) => a + b;
static double Restar(double a, double b) => a - b;
static double Multiplicar(double a, double b) => a * b;
static double Dividir(double a, double b) => b != 0 ? a / b : 0;

// 3. Usar el delegado
Operacion op = Sumar;           // Asignar un método al delegado
Console.WriteLine(op(10, 3));   // 13

op = Multiplicar;               // Cambiar el método asignado
Console.WriteLine(op(10, 3));   // 30

// Pasar como parámetro
static double Calcular(double a, double b, Operacion operacion)
{
    return operacion(a, b);
}

Console.WriteLine(Calcular(10, 5, Sumar));      // 15
Console.WriteLine(Calcular(10, 5, Restar));      // 5
Console.WriteLine(Calcular(10, 5, Dividir));     // 2

2. Delegados predefinidos: Action, Func, Predicate

En la práctica, casi nunca defines tus propios delegados. C# incluye 3 delegados genéricos que cubren casi todos los casos:

Action: método sin retorno (void)

// Action sin parámetros
Action saludar = () => Console.WriteLine("¡Hola!");
saludar();  // ¡Hola!

// Action con parámetros
Action<string> saludarA = nombre => Console.WriteLine($"Hola, {nombre}!");
saludarA("Ana");  // Hola, Ana!

// Action con múltiples parámetros
Action<string, int> mostrarInfo = (nombre, edad) =>
    Console.WriteLine($"{nombre} tiene {edad} años");
mostrarInfo("Luis", 30);

Func: método con retorno

// Func<retorno>
Func<int> obtenerNumero = () => 42;
Console.WriteLine(obtenerNumero());  // 42

// Func<param, retorno> - el ÚLTIMO tipo es siempre el retorno
Func<int, int, int> sumar = (a, b) => a + b;
Console.WriteLine(sumar(5, 3));  // 8

// Func<param, retorno>
Func<string, int> longitud = texto => texto.Length;
Console.WriteLine(longitud("Hola"));  // 4

// Func<param1, param2, retorno>
Func<double, double, double> potencia = Math.Pow;
Console.WriteLine(potencia(2, 10));  // 1024

Predicate: método que devuelve bool

// Predicate es equivalente a Func<T, bool>
Predicate<int> esPar = n => n % 2 == 0;
Console.WriteLine(esPar(4));  // True
Console.WriteLine(esPar(7));  // False

// Muy útil con métodos de colecciones
List<int> numeros = new() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

List<int> pares = numeros.FindAll(esPar);
Console.WriteLine(string.Join(", ", pares));  // 2, 4, 6, 8, 10

numeros.RemoveAll(n => n > 7);
Console.WriteLine(string.Join(", ", numeros));  // 1, 2, 3, 4, 5, 6, 7

3. Expresiones lambda

📘 Concepto: Una expresión lambda es una forma corta de escribir una función anónima (sin nombre). Usa la sintaxis => (“va a” o “produce”).

// Función normal
static int Duplicar(int x)
{
    return x * 2;
}

// Lambda equivalente
Func<int, int> duplicar = x => x * 2;

// Lambda con cuerpo (múltiples líneas)
Func<int, int> triplicar = x =>
{
    int resultado = x * 3;
    return resultado;
};

Sintaxis lambda resumida

Situación Ejemplo
Sin parámetros () => Console.WriteLine("Hola")
Un parámetro x => x * 2
Varios parámetros (a, b) => a + b
Con tipos explícitos (int a, int b) => a + b
Multilínea (a, b) => { var r = a + b; return r; }

Lambdas con colecciones

Las lambdas son la forma habitual de trabajar con colecciones:

List<string> nombres = new() { "Ana", "Luis", "María", "Pedro", "Carmen" };

// Buscar
string? primeroConM = nombres.Find(n => n.StartsWith("M"));       // "María"
List<string> cortosEn4 = nombres.FindAll(n => n.Length <= 4);      // "Ana", "Luis"

// Ordenar con criterio personalizado
nombres.Sort((a, b) => a.Length.CompareTo(b.Length));  // Por longitud

// Comprobar
bool hayMayorDe5 = nombres.Exists(n => n.Length > 5);  // True (Carmen)
bool todosMasDe2 = nombres.TrueForAll(n => n.Length > 2);  // True

// ForEach
nombres.ForEach(n => Console.WriteLine($"- {n}"));

// ConvertAll
List<int> longitudes = nombres.ConvertAll(n => n.Length);

4. Funciones de orden superior

📘 Concepto: Una función de orden superior es una función que recibe o devuelve otra función como parámetro.

// Función que recibe una función como parámetro
static List<T> Filtrar<T>(List<T> lista, Func<T, bool> condicion)
{
    List<T> resultado = new();
    foreach (T item in lista)
    {
        if (condicion(item))
            resultado.Add(item);
    }
    return resultado;
}

List<int> numeros = new() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var pares = Filtrar(numeros, n => n % 2 == 0);         // 2, 4, 6, 8, 10
var mayoresQue5 = Filtrar(numeros, n => n > 5);         // 6, 7, 8, 9, 10
var multiplos3 = Filtrar(numeros, n => n % 3 == 0);     // 3, 6, 9

// Función que DEVUELVE una función
static Func<double, double> CrearMultiplicador(double factor)
{
    return x => x * factor;
}

var duplicar = CrearMultiplicador(2);
var triplicar = CrearMultiplicador(3);

Console.WriteLine(duplicar(5));   // 10
Console.WriteLine(triplicar(5));  // 15

5. Eventos

📘 Concepto: Un evento es un mecanismo de notificación: un objeto avisa a otros cuando algo ocurre. Se basa en delegados. El patrón es publicador/suscriptor.

Crear y usar eventos

class Cronometro
{
    // Declarar el evento usando EventHandler
    public event EventHandler<int>? TicTac;

    public void Iniciar(int segundos)
    {
        for (int i = 1; i <= segundos; i++)
        {
            Thread.Sleep(1000);  // Espera 1 segundo

            // Lanzar el evento (notificar a los suscriptores)
            TicTac?.Invoke(this, i);
        }
    }
}

// Programa
var crono = new Cronometro();

// Suscribirse al evento con lambda
crono.TicTac += (sender, segundo) =>
{
    Console.WriteLine($"Tic... {segundo} segundos");
};

// Otra suscripción (pueden haber varias)
crono.TicTac += (sender, segundo) =>
{
    if (segundo == 3)
        Console.WriteLine("¡Ya van 3 segundos!");
};

crono.Iniciar(5);

Evento personalizado con datos

// Datos del evento
class PedidoEventArgs : EventArgs
{
    public string NombreProducto { get; set; } = "";
    public int Cantidad { get; set; }
    public DateTime Fecha { get; set; }
}

class Tienda
{
    public event EventHandler<PedidoEventArgs>? NuevoPedido;

    public void RealizarPedido(string producto, int cantidad)
    {
        Console.WriteLine($"Pedido realizado: {cantidad}x {producto}");

        // Notificar a los suscriptores
        NuevoPedido?.Invoke(this, new PedidoEventArgs
        {
            NombreProducto = producto,
            Cantidad = cantidad,
            Fecha = DateTime.Now
        });
    }
}

// Suscriptores
var tienda = new Tienda();

// Servicio de email
tienda.NuevoPedido += (sender, e) =>
    Console.WriteLine($"📧 Email enviado: Pedido de {e.Cantidad}x {e.NombreProducto}");

// Servicio de inventario
tienda.NuevoPedido += (sender, e) =>
    Console.WriteLine($"📦 Inventario actualizado: -{e.Cantidad} {e.NombreProducto}");

// Servicio de log
tienda.NuevoPedido += (sender, e) =>
    Console.WriteLine($"📋 Log: Pedido a las {e.Fecha:HH:mm:ss}");

tienda.RealizarPedido("Teclado", 2);
// Pedido realizado: 2x Teclado
// 📧 Email enviado: Pedido de 2x Teclado
// 📦 Inventario actualizado: -2 Teclado
// 📋 Log: Pedido a las 14:30:22

6. Ejercicios

Ejercicio 1: Calculadora con delegados

Crea un Dictionary<string, Func<double, double, double>> con operaciones (+, -, *, /, ^). El usuario elige operación y números, y se ejecuta el Func correspondiente.

Ejercicio 2: Pipeline de transformaciones

Crea una función Transformar que reciba un string y una lista de Func<string, string>. Aplica todas las transformaciones en orden. Ejemplo: quitar espacios → mayúsculas → revertir.

Ejercicio 3: Sistema de eventos - Chat

Crea una clase SalaChat con un evento MensajeRecibido. Varios Usuario se suscriben. Cuando un usuario envía un mensaje, todos los demás lo ven.

Ejercicio 4: Filtro configurable

Crea una función genérica FiltrarYTransformar<TInput, TOutput> que reciba una lista, un Predicate<TInput> para filtrar y un Func<TInput, TOutput> para transformar. Ejemplo: filtrar números pares y convertirlos a string.


Resumen

Concepto Ejemplo
Delegado propio delegate int Op(int a, int b);
Action (void) Action<string> a = s => Console.Write(s);
Func (con retorno) Func<int, int> f = x => x * 2;
Predicate (→ bool) Predicate<int> p = n => n > 0;
Lambda sin params () => 42
Lambda con params (a, b) => a + b
Evento public event EventHandler<T>? Evento;
Lanzar evento Evento?.Invoke(this, args);
Suscribirse obj.Evento += (s, e) => { };