Una expresión lambda es una pequeña función anónima escrita en línea con el operador => ("va a"). Los parámetros van a la izquierda y el resultado a la derecha:
Salida:
60.0
12
ORDER SHIPPED!
96.0, 180.0
Una lambda no tiene tipo propio. Se convierte en un delegado (un objeto que apunta a una función) del tipo delegado que espere el contexto: aquí Func<decimal, decimal>, un Predicate<T> para List.FindAll, un EventHandler para un evento. Los delegados Func devuelven un valor y los Action no; la página de Func y Action muestra la familia completa.
Formas de la sintaxis
x => x * x // one parameter: parentheses optional
(x, y) => x + y // several parameters
() => DateTime.Now.Year // no parameters
(int x, string s) => s.Length > x // explicit parameter types
order => // statement lambda: a block with return
{
decimal subtotal = order.Qty * order.Price;
return subtotal > 100m ? subtotal * 0.9m : subtotal;
}
Una lambda de expresión tiene una sola expresión después de =>, y su valor es el resultado. Una lambda de sentencias tiene un bloque entre llaves, que puede contener cualquier sentencia y debe hacer return de un valor en todos los caminos si el delegado devuelve uno (si no, CS1643, Not all code paths return a value in lambda expression).
Las versiones más recientes añadieron más formas, todas opcionales:
- C# 9: parámetros de descarte, de modo que varios parámetros sin usar pueden ser todos
_((_, _) => Save()), y lambdasstaticque tienen prohibido capturar variables. - C# 10: un "tipo natural", así que
var square = (int x) => x * x;deduceFunc<int, int>. Antes de eso,var f = x => x * 2;falla; incluso en C# 10 sigue fallando sin tipo de parámetro, con CS8917The delegate type could not be inferred. - C# 12: valores por defecto en los parámetros,
(int x, int y = 1) => x + y.
Pasar lambdas a métodos
Cualquier método que recibe un parámetro delegado acepta una lambda. Así es como pasas comportamiento, no solo datos:
Salida:
report.pdf, scan.pdf
photo.jpg, cv.txt, scan.pdf
cv.txt, scan.pdf, photo.jpg, report.pdf
Filter no sabe qué significa "conservar"; quien llama lo decide con una lambda. List.Sort funciona igual: la lambda es una comparación que devuelve un valor negativo, cero o positivo, y Sort la llama tantas veces como necesita.
Lambdas en LINQ
LINQ es donde la mayor parte del código C# se encuentra con las lambdas. Cada operador de consulta recibe una: Where una condición, Select una proyección, OrderBy una clave, Sum un valor que sumar:
Salida:
Ben: 250
Ana: 60
Cy: 45
Ana spent 80
Ben spent 250
Cy spent 45
Las lambdas no se ejecutan donde se escriben. LINQ las guarda y las llama cuando la consulta se enumera con foreach, ToList() o Sum(), lo que se llama ejecución diferida. La página de LINQ explica los propios operadores.
Closures: variables capturadas
Una lambda puede usar variables locales y parámetros del método en el que está escrita. Captura la variable, no su valor actual, y la variable sigue viva mientras viva la lambda:
Salida:
1
2
3
1
50.0
Cada llamada a MakeCounter crea un count nuevo, así que los dos contadores son independientes. La lambda del descuento leyó rate cuando se ejecutó, no cuando se escribió, así que aplicó un 50%. El compilador lo implementa moviendo las variables capturadas a una clase oculta que comparten el método y la lambda.
La trampa de la captura en bucles
Capturar la variable de un bucle es donde los closures muerden. Un bucle for tiene una variable para todo el bucle, así que cada lambda creada en él comparte esa variable:
Salida:
3 3 3 <- for loop, shared i
0 1 2 <- for loop, copied
0 1 2 <- foreach
Las tres lambdas del primer bucle se ejecutaron cuando el bucle ya había terminado, con i valiendo 3. Copiar i en una variable declarada dentro del cuerpo da a cada lambda la suya. foreach lo resuelve por sí solo: desde C# 5, su variable de iteración es una variable nueva en cada pasada. Antes de C# 5 tenía el mismo bug, y por eso algunas respuestas antiguas de internet siguen recomendando la copia también para foreach.
La misma trampa se aplica a todo lo que se ejecuta más tarde: manejadores de eventos, temporizadores, Task.Run y consultas LINQ construidas en un bucle y enumeradas después de él.
Lambdas, métodos anónimos y funciones locales
Las lambdas sustituyeron a la sintaxis de métodos anónimos de C# 2, que todavía compila:
Func<int, bool> isEven = delegate (int n) { return n % 2 == 0; }; // C# 2 anonymous method
Func<int, bool> isEven2 = n => n % 2 == 0; // lambda
Para un método auxiliar que solo se usa dentro de un método, C# 7.0 ofrece también las funciones locales, que tienen nombre, pueden ser recursivas y no crean un delegado salvo que las conviertas en uno. Usa una lambda cuando pasas comportamiento a algo (LINQ, Sort, un evento); usa una función local o un método privado cuando lo llamas tú mismo.
Hay una distinción más con los proveedores de LINQ como Entity Framework: una lambda asignada a Expression<Func<T, bool>> no se compila a código, sino a una estructura de datos que describe el código, y el proveedor la traduce a SQL. La sintaxis es idéntica; el tipo del parámetro del método al que llamas decide cuál obtienes.
Preguntas frecuentes
¿Qué es una expresión lambda en C#?
Una función anónima escrita con el operador =>: x => x * 2 recibe x y devuelve x * 2. Una lambda no tiene nombre propio; se convierte en un tipo delegado como Func<int, int> o Action<string> y puede guardarse en una variable, pasarse a un método o devolverse desde uno.
¿Cómo escribo una lambda con varios parámetros o sin parámetros?
Pon los parámetros entre paréntesis: (a, b) => a + b para dos, () => DateTime.Now para ninguno. Solo un parámetro único puede prescindir de los paréntesis (x => x + 1). También puedes indicar los tipos de forma explícita: (int a, int b) => a + b.
¿Qué es un closure en una lambda de C#?
Una lambda que usa una variable local del método que la rodea captura esa variable, no una instantánea de su valor. La variable vive tanto como la lambda, y si cualquiera de los dos lados la cambia después, el otro ve el cambio. Esto es lo que hace funcionar los contadores y los callbacks, y lo que provoca el clásico bug de captura del bucle for.
¿Por qué todas las lambdas de un bucle for ven el mismo valor?
Un bucle for tiene una sola variable de bucle para todo el bucle, así que cada lambda creada dentro captura el mismo i y ve su valor final cuando se ejecuta después. Cópiala en una variable local dentro del cuerpo (int copy = i;) y captura la copia. foreach no tiene este problema desde C# 5, porque crea una variable nueva en cada iteración.
¿Por qué var f = x => x * 2 no compila?
El compilador no puede saber qué tipo delegado quieres ni de qué tipo es x, así que da CS8917 The delegate type could not be inferred (antes de C# 10, CS0815). Declara el tipo (Func<int, int> f = x => x * 2;) o, en C# 10 y posteriores, indica el tipo del parámetro: var f = (int x) => x * 2;.