Un delegado es un tipo que representa la firma de un método. Una variable de un tipo delegado contiene una referencia a un método con esa firma, y llamar a la variable llama al método. Así es como C# trata las funciones como valores: puedes guardarlas, pasarlas a otros métodos y llamarlas más tarde.
Salida:
40
70
68.00
delegate decimal PriceRule(decimal price); declara un tipo nuevo, igual que hace class. Cualquier método que reciba un decimal y devuelva un decimal encaja en él. La variable rule apuntó a tres métodos distintos por turnos, y cada llamada ejecutó el que contenía en ese momento.
Declarar, crear e invocar
La declaración de un tipo delegado parece la firma de un método con la palabra clave delegate delante. Normalmente está a nivel de namespace, junto a las clases, o anidada dentro de una clase.
Hay cuatro formas de crear una instancia de delegado:
PriceRule a = HalfPrice; // method group (most common)
PriceRule b = new PriceRule(HalfPrice); // explicit constructor, same thing
PriceRule c = p => p - 5m; // lambda expression
PriceRule d = delegate (decimal p) { return p; }; // anonymous method (C# 2 syntax)
Y dos formas de llamarla, que son idénticas: rule(80m) y rule.Invoke(80m). El compilador comprueba la firma al asignar, así que un método que no encaja es un error de compilación y no una sorpresa en tiempo de ejecución.
Un delegado también puede apuntar a un método de instancia. En ese caso recuerda tanto el método como el objeto sobre el que llamarlo:
Salida:
Ana got: Your order shipped
target: Mailbox, method: Receive
Target es el objeto sobre el que el delegado llamará al método (null en un método static), y Method describe el método. Guardar el objeto de destino también significa que un delegado mantiene vivo ese objeto, algo que importa con los eventos.
Delegados como callbacks
La función principal de un delegado es permitir que un método llame a código que eligió quien lo llama. El método define cuándo ocurre algo; quien llama define qué:
Salida:
[## ] 25%
[##### ] 50%
[####### ] 75%
[##########] 100%
import finished
ImportRows no sabe nada de consolas ni de barras. El mismo método puede mover una barra de progreso, escribir una línea de log o actualizar una interfaz, según lo que pase quien llama. List.Sort(Comparison<T>), Array.Find(Predicate<T>), Task.Run(Action) y todos los operadores de LINQ siguen este patrón.
Delegados multicast: += y -=
Un delegado puede contener más de un método. += añade un método a su lista de invocación, -= lo quita, e invocar el delegado llama a todos los métodos en orden:
Salida:
email sent for A-1001
stock updated for A-1001
warehouse notified for A-1001
3 handlers
email sent for A-1002
warehouse notified for A-1002
Tres detalles de los delegados multicast importan en la práctica:
- Valores de retorno: si el tipo delegado devuelve un valor, invocar un delegado multicast devuelve solo el resultado del último método. Para reunir todos los resultados, recorre
GetInvocationList()e invoca cada uno tú mismo. - Excepciones: si un método lanza una excepción, los métodos restantes no se ejecutan y la excepción llega a quien llama.
- Inmutabilidad: los delegados son inmutables.
+=crea un delegado nuevo con una lista más larga y lo vuelve a asignar a la variable, por eso quitar con-=sobre una copia no afecta al original.
Quitar una lambda con -= solo funciona si pasas la misma instancia de delegado. Escribir la misma lambda dos veces crea dos delegados distintos, así que handlers -= id => Log(id); no quita nada. Guarda la lambda en una variable cuando pienses cancelar la suscripción.
Delegados null y ?.Invoke
Una variable delegado sin nada asignado es null, y quitar el último método con -= también la deja en null. Invocar null lanza NullReferenceException:
Salida:
no handler, no crash
saved draft.txt
NullReferenceException
?.Invoke es la forma habitual de llamar a un callback opcional. El operador ?. no puede ponerse directamente antes de los paréntesis (onSaved?("x") no es sintaxis válida), así que el patrón llama por su nombre al método Invoke del delegado, que hace lo mismo que la llamada abreviada.
Delegados, Func, Action y eventos
Declarar un tipo delegado para cada firma sería tedioso, así que .NET incluye tipos genéricos:
| Tipo incorporado | Declaración propia equivalente |
|---|---|
Action | delegate void Action(); |
Action<T> | delegate void Action<T>(T arg); |
Func<TResult> | delegate TResult Func<TResult>(); |
Func<T, TResult> | delegate TResult Func<T, TResult>(T arg); |
Predicate<T> | delegate bool Predicate<T>(T obj); |
PriceRule, del primer ejemplo, tiene la misma forma que Func<decimal, decimal>, y aun así son tipos distintos que no se convierten entre sí. Ni siquiera dos tipos incorporados con firmas idénticas se convierten:
Func<int, bool> isPositive = x => x > 0;
Predicate<int> p = isPositive;
// error CS0029: Cannot implicitly convert type 'System.Func<int, bool>' to 'System.Predicate<int>'
Predicate<int> ok = new Predicate<int>(isPositive); // wrap it explicitly
Predicate<int> ok2 = x => isPositive(x); // or with a lambda
Una lambda se convierte a cualquier tipo delegado compatible, así que las API suelen recibir lambdas y la cuestión nunca surge. La página de Func y Action explica la familia genérica y cuándo sigue mereciendo la pena una declaración propia.
Los eventos se construyen directamente sobre los delegados. Un campo event es un delegado multicast al que el código externo solo puede aplicar += y -=, nunca invocarlo ni sobrescribirlo. La lista de suscriptores del ejemplo multicast de arriba es exactamente lo que un evento gestiona por ti, con esa protección extra. Los eventos tienen su propia página.
Preguntas frecuentes
¿Qué es un delegado en C#?
Un delegado es una referencia a un método con seguridad de tipos. Declaras un tipo delegado con una firma, como delegate decimal PriceRule(decimal price);, y cualquier método con esa firma (static, de instancia o una lambda) puede guardarse en una variable de ese tipo y llamarse después a través de ella. Los delegados son la forma en que C# pasa funciones como valores.
¿Qué diferencia hay entre un delegado y Func en C#?
Func<T, TResult> y Action<T> son tipos delegados que .NET ya declara por ti, genéricos en los tipos de los parámetros y del retorno. Una declaración delegate propia da al tipo un nombre y unos nombres de parámetros que documentan su papel, y admite parámetros ref, out e in. En la mayoría del código, Func y Action bastan.
¿Qué es un delegado multicast en C#?
Un delegado que contiene varios métodos. += añade un método a su lista de invocación y -= quita uno; invocar el delegado los llama todos en orden. Si el delegado devuelve un valor, quien llama solo recibe el resultado del último método, y una excepción en un método impide que se ejecuten los demás.
¿Cómo invoco de forma segura un delegado que podría ser null?
Usa el operador condicional null: onProgress?.Invoke(50);. Una variable delegado sin métodos asignados es null, y llamarla directamente lanza NullReferenceException. ?.Invoke lee la variable una sola vez, así que también es seguro si otro hilo quita el último manejador en ese mismo momento.
¿Cuándo debo declarar mi propio tipo delegado en lugar de usar Func o Action?
Cuando la firma necesita parámetros ref, out o in (Func y Action no pueden expresarlos), cuando un tipo con nombre aclara una API pública (delegate bool Validator(string input)), o cuando quieres que los nombres de los parámetros aparezcan en IntelliSense. En los demás casos, prefiere Func y Action, que todo desarrollador de .NET reconoce.