1. Introducción
¿Sabes qué es curioso? La expresión lambda en sí misma es como una receta sin olla. Describe qué hacer, pero necesita un "contenedor" para vivir en el código. En C# hay tipos genéricos de delegados listos para esto: Func, Action y Predicate.
Imagínatelos como moldes listos para tus lambdas: eliges el adecuado y viertes tu lógica. Nada de reinventar la rueda ni definir delegados personalizados cuando ya tienes todo lo necesario.
Un poco de historia
Al principio, cuando hacía falta pasar una función como parámetro, había que declarar un tipo de delegado propio. Era largo, tedioso y se veía así:
delegate int Calculate(int x, int y);
Calculate adder = (a, b) => a + b;
Cuando en C# aparecieron Func, Action y Predicate, pudiste olvidarte de declarar delegados a mano en el 90% de los casos. Ahora se ve mucho más sencillo y genérico:
Func<int, int, int> adder = (a, b) => a + b;
2. Func<T, TResult> — función con retorno
Sintaxis y propósito
Func es un delegado genérico que acepta de 0 a 16 parámetros y devuelve un valor.
Func<int, int, int> sum = (x, y) => x + y;
Func<tipo1, tipo2, ..., tipoN, TResult> — todos los parámetros hasta el último son los argumentos, el último es el tipo devuelto.
Ejemplos
1. Suma de dos números
Func<int, int, int> sum = (x, y) => x + y;
Console.WriteLine(sum(3, 5)); // 8
2. Elevar un número al cuadrado
Func<int, int> square = x => x * x;
Console.WriteLine(square(4)); // 16
3. Sin parámetros
Func<string> greet = () => "Hello, lambda!";
Console.WriteLine(greet());
Esquema visual
| Firma | Ejemplo | Descripción |
|---|---|---|
|
|
Acepta int, devuelve int |
|
|
Dos int, devuelve int |
|
|
No acepta nada, devuelve una cadena |
Cómo se ve en tu aplicación
Supongamos que en nuestra mini-aplicación (el hipotético "Directorio de usuarios") tenemos una lista de números — queremos aplicarles diferentes transformaciones. Por ejemplo, elevar al cuadrado o sumar con un número fijo:
List<int> numbers = new() { 1, 2, 3, 4, 5 };
Func<int, int> square = x => x * x;
var squares = numbers.Select(square);
Console.WriteLine(string.Join(", ", squares)); // 1, 4, 9, 16, 25
3. Action<T> — acción sin retorno
Action es un delegado genérico para métodos que hacen algo (por ejemplo, imprimen en pantalla) pero no devuelven nada.
Puede aceptar de 0 a 16 parámetros, pero siempre devuelve void.
Ejemplos
1. Impriworld en pantalla
Action<string> print = text => Console.WriteLine("Data: " + text);
print("Hello, world!");
2. Acción sin parámetros
Action greet = () => Console.WriteLine("Welcome!");
greet();
3. Acción con varios parámetros
Action<int, int> showSum = (a, b) => Console.WriteLine($"Sum: {a + b}");
showSum(2, 3); // Sum: 5
Esquema visual
| Firma | Ejemplo | Descripción |
|---|---|---|
|
|
Sin parámetros, sin retorno |
|
|
Un parámetro |
|
|
Varios parámetros |
En nuestra aplicación
Añadamos al directorio de usuarios un método para impriworld todos los nombres:
List<string> names = new() { "Anna", "Boris", "Vika" };
Action<string> printName = name => Console.WriteLine("User: " + name);
names.ForEach(printName);
// o así: names.ForEach(name => Console.WriteLine(name));
4. Predicate<T> — ¿sí o no?
Cuando necesitas una función que devuelva solo true o false para un parámetro, usa Predicate<T>. Es un delegado que acepta un parámetro y devuelve bool.
Predicate es la envoltura oficial de "necesitamos una comprobación booleana" para lambdas.
Ejemplos
1. Comprobar si un número es mayor que 5
Predicate<int> isGreaterThanFive = x => x > 5;
Console.WriteLine(isGreaterThanFive(3)); // false
Console.WriteLine(isGreaterThanFive(7)); // true
2. Uso con el método List<T>.Find
List<int> values = new() { 2, 4, 7, 10 };
int found = values.Find(isGreaterThanFive); // usa Predicate<int>
Console.WriteLine(found); // 7
3. ¿Todos son adultos?
List<int> ages = new() { 12, 19, 34 };
bool allAdults = ages.TrueForAll(age => age >= 18);
// TrueForAll acepta Predicate<int>
¿En qué se diferencia de Func<T, bool>?
En la práctica, son totalmente intercambiables. Incluso la documentación de Microsoft dice: "Predicate<T> es simplemente Func<T, bool> para APIs especiales". Pero algunos métodos de la librería estándar piden específicamente Predicate.
5. Cómo las lambdas "encajan" en Func, Action, Predicate
Cuando escribes una lambda, C# analiza: "¡Ah, su forma coincide con el delegado requerido — se puede asignar!"
Func<int, int> f1 = x => x * 2;
Action<string> a1 = text => Console.WriteLine(text);
Predicate<int> p1 = x => x < 10;
Lambdas por todas partes. Pero bajo el capó son tres delegados distintos con firmas diferentes.
Aplicación con código "real"
List<User> users = new() {
new User("Anna", 24),
new User("Boris", 17),
new User("Vika", 31),
};
// Función que devuelve solo usuarios adultos (Predicate<User>)
List<User> adults = users.FindAll(user => user.Age >= 18);
Console.WriteLine("List of adults: " + string.Join(", ", adults.Select(u => u.Name)));
Y si queremos impriworld los nombres de todos los usuarios con Action<User>:
users.ForEach(user => Console.WriteLine(user.Name));
Obtener sus nombres (Func<User, string>):
IEnumerable<string> names = users.Select(user => user.Name);
Tabla para mayor claridad
| Delegado | Firma | Ejemplo de lambda | Dónde se usa |
|---|---|---|---|
|
T → U | |
Select, cualquier transformación |
|
T → void | |
ForEach, métodos-acción |
|
T → bool | |
Find, Exists, filtros |
6. Ejemplos con la aplicación interna: paso a paso
Vamos a ampliar nuestra pequeña aplicación "Directorio de usuarios". Supongamos que tenemos la clase User:
public class User
{
public string Name { get; }
public int Age { get; }
public bool IsActive { get; set; }
public User(string name, int age)
{
Name = name;
Age = age;
IsActive = true;
}
}
1. Func<User, bool> — comprobamos si el usuario es mayor de edad
Func<User, bool> isAdult = user => user.Age >= 18;
Usamos en LINQ:
var adults = users.Where(isAdult);
2. Predicate<User> — buscamos un usuario activo
Predicate<User> isActive = user => user.IsActive;
User found = users.Find(isActive);
3. Action<User> — desactivamos a un usuario
Action<User> deactivate = user => user.IsActive = false;
users.ForEach(deactivate);
4. Func<User, string> — obtenemos una descripción corta
Func<User, string> describe = user => $"{user.Name} ({user.Age})";
var descriptions = users.Select(describe);
Todas estas lambdas son código en forma de datos: se pueden pasar a métodos, guardar en variables y combinar.
7. Matices no obvios y errores típicos
1. La lambda debe coincidir con la firma del delegado. Si la firma no coincide, habrá error de compilación.
Func<int, string> wrong = x => x * 2; // error: se esperaba string, se obtuvo int
// Correcto:
Func<int, string> right = x => (x * 2).ToString();
2. No olvides void vs return. Action no devuelve valor — intentar algo como Action<int> a = x => x * x; no funcionará, porque devuelve un valor cuando no debería.
3. Predicate<T> y Func<T, bool> a menudo son intercambiables, pero no siempre. A veces los métodos de colección esperan exactamente Predicate<T>, otras veces Func<T, bool>. La asignación directa no funciona sin un envoltorio explícito.
Predicate<int> pred = x => x > 0;
Func<int, bool> func = pred; // error
// Pero:
Func<int, bool> func2 = x => x > 0;
Predicate<int> pred2 = new Predicate<int>(func2); // se puede convertir vía constructor
GO TO FULL VERSION