CodeGym /Cursos /C# SELF /Delegados: escenarios avanzados

Delegados: escenarios avanzados

C# SELF
Nivel 54 , Lección 3
Disponible

1. Callback y programación asíncrona

Callback — es un mecanismo para pasar un método que debe ser llamado al terminar alguna operación. Muy usado en operaciones asíncronas, timers, procesamiento de datos, interfaces de usuario.

Ejemplo 1: Operación asíncrona con callback

Supongamos que tenemos una aplicación donde el usuario ingresa una consulta, y el resultado llega con retraso (por ejemplo, desde internet). Después de obtener los datos, queremos actualizar la pantalla.

// Delegado para callback
public delegate void DataReceivedHandler(string result);

// Mecanismo de descarga de datos asíncrono (simulación)
public void DownloadDataAsync(DataReceivedHandler callback)
{
    // Supongamos que la descarga tarda (simulado con timer)
    Task.Delay(1000).ContinueWith(_ =>
    {
        string data = "Resultados de búsqueda: <datos>";
        callback(data); // Llamada al delegado-callback
    });
}

// Uso:
DownloadDataAsync(result =>
{
    Console.WriteLine("Recibido: " + result);
});

Este enfoque permite escribir código muy flexible, donde la lógica después de obtener el resultado está completamente separada de la mecánica de obtención de datos.

2. Delegados como parámetros en métodos: estrategia y comparadores

Una tarea frecuente: permitir que el usuario pase “lógica” (función) a tu método, para que pueda decidir cómo comparar, filtrar o transformar elementos.

Ejemplo 2: Implementación del patrón Strategy usando delegados

Supongamos que tenemos una ordenación, pero queremos que el usuario pueda ordenar de diferentes maneras — por nombre, por fecha, por tamaño, etc.

public delegate bool CompareFunc(int a, int b);

public void BubbleSort(int[] arr, CompareFunc compare)
{
    for (int i = 0; i < arr.Length; i++)
    {
        for (int j = 0; j < arr.Length - 1; j++)
        {
            if (compare(arr[j], arr[j + 1]))
            {
                // Intercambiar
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

// Orden descendente
CompareFunc descending = (a, b) => a < b;

// Uso
int[] numbers = { 3, 1, 4, 2 };
BubbleSort(numbers, descending);
Console.WriteLine(string.Join(", ", numbers)); // Salida: 4, 3, 2, 1

Este método — una forma universal de integrar tu “estrategia” en código externo sin modificar su fuente.

3. Métodos anónimos, expresiones lambda y delegados

Con el avance de C#, ya no es cómodo crear clases o métodos separados para cada tarea. Por suerte, aparecieron métodos anónimos y expresiones lambda, que permiten crear delegados “sobre la marcha”.

Ejemplo 3: Lambda como delegado

Func<int, int, int> operation = (x, y) => x * y;
int result = operation(3, 5); // 15

Ejemplo 4: Selección de operación por nombre (Switch + Delegates)

Func<int, int, int> op;
string userInput = "sum"; // "sub", "mul", "div"

switch (userInput)
{
    case "sum": op = (a, b) => a + b; break;
    case "sub": op = (a, b) => a - b; break;
    case "mul": op = (a, b) => a * b; break;
    case "div": op = (a, b) => a / b; break;
    default: throw new Exception("¡Operación desconocida!");
}
Console.WriteLine(op(6, 2));

No olvides validar la entrada — los delegados ofrecen flexibilidad y claridad.

4. Delegados y cadenas de procesamiento (“cadena de responsabilidades”)

Como los delegados soportan multicast, permiten construir fácilmente cadenas de manejadores.

Ejemplo 5: Cadena de filtros

Supón que tienes “filtros” que deben procesar una cadena de texto.

public delegate string StringFilter(string input);

string RemoveDigits(string input) => new string(input.Where(ch => !char.IsDigit(ch)).ToArray());
string ToUpper(string input) => input.ToUpper();

StringFilter filters = RemoveDigits;
filters += ToUpper;

// La variable delegate recorrerá la cadena a través de todos los filtros
string text = "Hola123";
foreach (StringFilter filter in filters.GetInvocationList())
{
    text = filter(text);
}
Console.WriteLine(text); // Salida: "HOLA"

Importante: si llamas simplemente filters(text), solo obtienes el resultado del último manejador, no la cadena completa. Para “propagar” el valor, usa la iteración explícita como en el ejemplo.

5. Delegados para vinculación dinámica de comportamiento en tiempo de ejecución

Antes, para cambiar un comportamiento, había que crear clases e interfaces específicas. Ahora, con delegados y lambdas, se puede implementar casi todo el “polimorfismo pequeño” usando delegados.

Ejemplo 6: Comportamiento de un robot con comando dinámico

Supón que en un juego estamos desarrollando un “robot” que reacciona a la señal “Comando recibido”. En lugar de crear clases, hacemos esto con funciones.

public class Robot
{
    public Action<string>? OnCommandReceived;

    public void ReceiveCommand(string command)
    {
        OnCommandReceived?.Invoke(command);
    }
}

// Uso:
var robot = new Robot();

robot.OnCommandReceived += cmd => Console.WriteLine($"El robot ejecuta: {cmd}");
robot.OnCommandReceived += cmd =>
{
    if (cmd == "Encender")
        Console.WriteLine("Cargando sistema...");
};
// Probamos
robot.ReceiveCommand("Encender");
robot.ReceiveCommand("Mover hacia adelante");

Este patrón se usa mucho en tests, prototipos, en contenedores de DI (dependency injection) y para pasar lógica de negocio como parámetros.

6. Delegados como suscripciones a estados

Supón que tienes una clase que guarda un estado, y quieres notificar a todos los suscriptores cuando cambie. Gracias a delegados (y eventos), esto es sencillo.

Ejemplo 7: Clase con suscripción a cambios

public class Notifier<T>
{
    private T _value = default!;
    public event Action<T>? ValueChanged;

    public T Value
    {
        get => _value;
        set
        {
            if (!Equals(_value, value))
            {
                _value = value;
                ValueChanged?.Invoke(_value);
            }
        }
    }
}

// Uso:
var intValue = new Notifier<int>();
intValue.ValueChanged += v => Console.WriteLine($"Nuevo valor: {v}");
intValue.Value = 5; // dispara evento
intValue.Value = 10;

Este método — casi “programación reactiva básica”, base para MVVM, data binding y muchos frameworks UI modernos.

7. Delegados, closures y ámbito léxico

Las expresiones lambda y métodos anónimos pueden capturar variables del contexto externo (“closure”). Esto puede ser útil, pero también puede causar errores inesperados.

Ejemplo 8: Captura de variable y “trampa” en bucle

Action[] actions = new Action[3];

for (int i = 0; i < 3; i++)
{
    actions[i] = () => Console.WriteLine(i);
}

foreach (var a in actions)
    a(); // Imprime tres veces 3 (!)

¿Por qué? Porque el closure referencia a la misma variable i, que después del ciclo vale 3. ¿Y si quieres guardar los valores 0, 1, 2?

for (int i = 0; i < 3; i++)
{
    int valorDeCiclo = i; // “congelamos” el valor actual
    actions[i] = () => Console.WriteLine(valorDeCiclo);
}

Así, el código funciona como se espera.

¡Estas trampas son uno de los errores más comunes para principiantes con lambdas!

8. Combinación de delegados

Los multicast-delegates contienen una lista de métodos, y puedes agregar (+=) o quitar (-=) manejadores.

Característica: se eliminan por referencia y firma

void Handler1() => Console.WriteLine("1");
void Handler2() => Console.WriteLine("2");

Action a = Handler1;
a += Handler2;
a -= Handler1; // Solo queda Handler2
a?.Invoke();   // Imprime "2"

Si añades varias veces el mismo método, al quitarlo solo se elimina una instancia.

Ejemplo: gestión dinámica de manejadores

Action a = Handler1;
a += Handler1;
a -= Handler1; // Queda SOLO UN Handler1 en la lista

9. Delegados para extensión y inversión de control (IoC)

En aplicaciones grandes, a menudo quieres que los componentes puedan “llamar” código externo, manteniendo independencia. Los delegados ayudan a integrar “extensiones”, “plugins”, “callbacks” y cadenas sin acoplamiento fuerte.

Ejemplo: Inyección de comportamiento en constructor

public class Greeter
{
    private readonly Func<string> _getName;

    public Greeter(Func<string> getName)
    {
        _getName = getName;
    }

    public void Greet() => Console.WriteLine($"¡Hola, {_getName()}!");
}

// Inyección de diferentes comportamientos:
var greeter1 = new Greeter(() => "Ana");
var greeter2 = new Greeter(() => DateTime.Now.ToShortTimeString());

greeter1.Greet(); // "¡Hola, Ana!"
greeter2.Greet(); // "¡Hola, 14:35!"

En la vida real, este patrón se usa para escribir código testeable y mantenible.

10. Consejos útiles

Delegados en interfaces estándar y LINQ

Encontrarás delegados en LINQ, colecciones, asincronía.

  • Todos los métodos List<T>.Find, Array.Sort, Where, Select aceptan delegados (Func<T, bool>, Comparison<T> y otros).
  • Los métodos LINQ trabajan con delegados y permiten pasar lógica de filtrado, transformación, agregación — sin crear clases separadas.

Ejemplo: Comparador para ordenar objetos

var personas = new[] { "Ivan", "Maria", "Pyotr" };
Array.Sort(personas, (a, b) => a.Length.CompareTo(b.Length));
Console.WriteLine(string.Join(", ", personas)); // Ivan, Pyotr, Maria

Delegados y currying (aplicación parcial de argumentos)

Con métodos anónimos o lambdas, puedes “fijar” algunos parámetros y crear nuevas funciones.

Ejemplo: Aplicación parcial

Func<int, int, int> sum = (x, y) => x + y;

// Crear función que siempre suma 10
Func<int, int> add10 = y => sum(10, y);

Console.WriteLine(add10(5)); // 15

En lenguajes funcionales, es una técnica estándar; en C# es posible gracias a los delegados.

Comparación de delegados

En C#, los delegados se pueden comparar por igualdad si tienen la misma lista de invocación.

Action a1 = Handler1;
Action a2 = Handler1;
Console.WriteLine(a1 == a2); // True

Action a3 = Handler1; a3 += Handler2;
Action a4 = Handler1; a4 += Handler2;
Console.WriteLine(a3 == a4); // True

Pero si el delegado es una lambda o método anónimo, se comparan las instancias.

Serialización de delegados

Se pueden serializar, pero solo si los métodos son en clases serializables y todos los tipos son accesibles.

En .NET 9, BinaryFormatter fue eliminado oficialmente, y la serialización de delegados en producción casi no se usa.

Delegados y eventos: ¿dónde está cada uno?

  • Delegado — tipo, variable que puede invocarse explícitamente.
  • Evento (event) — restringe el acceso al delegado: desde fuera solo puedes suscribirte o cancelar (+=/-=), pero solo el interior de la clase puede invocarlo.
  • Un evento siempre es tipo delegado, pero no todo delegado es evento.

¿Cómo usar? Si quieres que la lógica pueda definirse “desde fuera”, usa delegados. Si quieres controlar suscripción y proteger la variable, usa eventos.

11. Errores comunes y trampas en escenarios avanzados

  • Fugas de memoria: ¿olvidaste cancelar la suscripción a eventos en objetos que viven mucho? — ¡El suscriptor no se eliminará!
  • Captura de variable en bucle: la lambda en un ciclo captura la variable externa, no su valor actual.
  • Pérdida de suscriptores: pasar un delegado a un campo o propiedad sin evento, pierde control sobre los suscriptores y llamadas.
  • Mal manejo de multicast-delegates: si los manejadores devuelven valores, al llamar a todos solo se obtiene el resultado del último.
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION