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,Selectaceptan 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.
GO TO FULL VERSION