Func y Action son tipos delegados genéricos que .NET declara por ti, así que puedes guardar y pasar funciones sin declarar antes un tipo delegado. Func devuelve un valor; Action no devuelve nada.
Salida:
4
46.00
False
Hello!
Mia scored 87
La regla que hay que recordar: en un Func, el último argumento de tipo es el tipo de retorno. Func<int, int, bool> recibe dos int y devuelve un bool. Func<int> no recibe nada y devuelve un int. En un Action, todos los argumentos de tipo son parámetros.
La familia
| Tipo | Parámetros | Devuelve |
|---|---|---|
Action | ninguno | void |
Action<T> | T | void |
Action<T1, T2> ... hasta 16 | T1, T2, ... | void |
Func<TResult> | ninguno | TResult |
Func<T, TResult> | T | TResult |
Func<T1, T2, TResult> ... hasta 16 | T1, T2, ... | TResult |
Predicate<T> | T | bool |
Comparison<T> | T, T | int (negativo, cero, positivo) |
Converter<TIn, TOut> | TIn | TOut |
Predicate, Comparison y Converter son más antiguos y aparecen en métodos de List<T> y de Array: FindAll(Predicate<T>), Sort(Comparison<T>), ConvertAll(Converter<T, TOut>). LINQ usa Func en todas partes, por eso list.Where(...) y list.FindAll(...) reciben lambdas de la misma forma pero de tipos distintos:
Salida:
17, 15
34, 52, 28
52, 34, 28, 17, 15
52y 34y 28y 17y 15y
Pasar funciones a métodos
Un parámetro Func o Action permite que quien llama aporte la parte de un algoritmo que cambia. Dos formas habituales son "ejecuta esto y mídelo" y "prueba esto hasta que funcione":
Salida:
attempt 1 failed
attempt 2 failed
<html>ok</html>
clicks: 2
Retry es genérico, así que su Func<int, T> puede devolver cualquier cosa; el compilador dedujo T como string a partir de la lambda. La lambda que se pasa a Twice cambia la variable local clicks, y funciona porque una lambda captura la propia variable, no una copia de su valor.
Grupos de métodos: pasar un método por su nombre
No necesitas una lambda para rellenar un Func o un Action. El nombre de un método existente con una firma compatible (un "grupo de métodos") se convierte directamente:
Salida:
parsed: 43
sat is weekend: True
total quantity: 20
Select(int.Parse) es lo mismo que Select(s => int.Parse(s)), con una capa de llamada menos. El compilador elige la sobrecarga que encaja con el tipo de destino: int.Parse tiene varias sobrecargas, y solo Parse(string) coincide con Func<string, int>. Cuando el compilador no puede decidirse por una (varias sobrecargas encajan igual de bien, o los argumentos de tipo de un método genérico no pueden deducirse de un grupo de métodos), la llamada no compila, y una lambda que haga explícita la llamada lo resuelve.
Un grupo de métodos usado como Func sigue ejecutándose más tarde, cada vez que se invoca el delegado, exactamente igual que una lambda. La diferencia está solo en cómo se escribe.
Devolver funciones
Un método puede construir y devolver una función. El Func devuelto lleva consigo los valores a partir de los que se construyó:
Salida:
140
180
162
Twice(staff) construye una función nueva que aplica dos veces el descuento de empleado: 200, después 180 y después 162.
Un diccionario de funciones
Guardar funciones en un diccionario convierte un switch largo en una tabla de búsqueda. Es la forma habitual de los manejadores de comandos, las calculadoras y las acciones de menú, y se pueden añadir entradas nuevas en tiempo de ejecución:
Salida:
12 + 30 = 42
7 * 6 = 42
9 / 0 = cannot divide by zero
2 ^ 3 = unknown operator
Hello, Sam
DONE
Añadir un operador es una entrada más en el diccionario, sin cambiar el bucle. El StringComparer.OrdinalIgnoreCase que se pasa al segundo diccionario hace que los nombres de los comandos no distingan mayúsculas y minúsculas.
Funciones async: Func<Task>, no Action
Con código async, el tipo delegado decide si quien llama puede esperar al trabajo:
Func<Task> save = async () => await File.WriteAllTextAsync("a.txt", "data");
await save(); // the caller can await it and see its exceptions
Func<int, Task<string>> load = async id => await FetchUserAsync(id);
string user = await load(42);
Action bad = async () => await File.WriteAllTextAsync("a.txt", "data");
bad(); // async void: fire and forget, exceptions escape
Una lambda async asignada a un Action se convierte en un método async void: nada puede esperarla, y una excepción dentro de ella nunca llega a quien llama. Se vuelve a lanzar en el pool de hilos (o en el hilo de la interfaz en una aplicación de escritorio), donde normalmente hace caer el proceso. Cuando escribas un método que acepte trabajo async, recibe un Func<Task> (o Func<Task<T>>).
Cuándo declarar tu propio delegado
Func y Action cubren casi todas las necesidades. Declara un tipo delegado con nombre cuando:
- La firma tiene parámetros
ref,outoin.Funcno puede expresarlos, así que una función al estilo deTryParsenecesitadelegate bool TryParser<T>(string text, out T value);. - El nombre documenta un papel en una API pública.
delegate bool Validator(string input)en la firma de un método dice más queFunc<string, bool>, y los nombres de sus parámetros aparecen en IntelliSense. - Necesitas un parámetro
params, que los delegados genéricos tampoco pueden tener.
Los eventos son el otro sitio donde siguen apareciendo tipos delegados propios, aunque el EventHandler<TEventArgs> incorporado cubre la mayoría. La página de delegados explica cómo declarar y combinar tipos delegados.
Preguntas frecuentes
¿Qué diferencia hay entre Func y Action en C#?
Func devuelve un valor y Action no. En Func<int, string, bool>, el último argumento de tipo (bool) es el tipo de retorno y los demás son parámetros. Action<int, string> recibe un int y un string y devuelve void. Los dos existen en versiones de 0 a 16 parámetros.
¿Cómo paso una función como parámetro en C#?
Declara el parámetro como un Func o un Action de la forma adecuada y llámalo dentro del método: static decimal Apply(decimal price, Func<decimal, decimal> rule) => rule(price);. Quien llama pasa una lambda (Apply(80m, p => p * 0.9m)) o el nombre de un método (Apply(80m, HalfPrice)).
¿Qué es Predicate<T> en C#?
Un tipo delegado que recibe un T y devuelve bool, con la misma forma que Func<T, bool>. Es anterior a Func y lo usan List<T>.Find, FindAll, RemoveAll, Exists y Array.Find. Los dos tipos no se convierten entre sí, pero una lambda se convierte a cualquiera de ellos.
¿Cómo uso Func con código async?
Usa Func<Task> para una función async sin resultado y Func<T, Task<TResult>> para una con resultado, de modo que quien llama pueda hacer await. Evita asignar una lambda async a un Action: eso la convierte en async void, que no se puede esperar y cuyas excepciones no puede capturar quien llama.
¿Puede Func tener parámetros out o ref?
No. Los parámetros genéricos de Func y Action son parámetros por valor normales, así que no hay forma de escribir Func<string, out int, bool>. Para una firma como la de TryParse, declara un delegado propio: delegate bool TryParser<T>(string text, out T value);.