1. Introducción
Hasta ahora viste que llamábamos métodos de LINQ que aceptaban una lambda como entrada. Pero, ¿cómo sabe un método de C# que allí se le puede pasar una función?
En pocas palabras, los delegados son como una interfaz para funciones: describimos la firma (tipos de parámetros y valor de retorno), y cualquier método (¡y lambda!) que coincida con esa firma puede ser pasado donde se espera ese delegado.
¿Recuerdas cómo pasabas una cadena como parámetro? Pues con la “lógica” es casi lo mismo, solo que el tipo del parámetro es un delegado.
Delegados: teoría básica en cristiano
En C# un delegado es un tipo que describe “una función con tal firma”.
// Delegado que acepta int y devuelve bool
public delegate bool IntPredicate(int x);
Cualquier función compatible por firma puede asignarse a una variable de ese tipo:
bool IsEven(int n) => n % 2 == 0;
IntPredicate pred = IsEven;
Y ahora — una lambda encaja:
IntPredicate pred = x => x % 2 == 0;
Delegados genéricos: Func, Action, Predicate
- Func<T1, ..., TResult> — función que acepta parámetros T1, ... y devuelve TResult.
- Action<T1, ...> — función que acepta parámetros y no devuelve valor (void).
- Predicate<T> — función que acepta T y devuelve bool.
2. Pasar una lambda a tu método
Imagina que estamos desarrollando nuestra mini-aplicación de aprendizaje — un proyecto de consola que trabaja con una lista de usuarios. Antes filtrábamos colecciones con LINQ, ahora escribiremos nuestro propio método que acepta una lambda-condición.
Creamos nuestro método con un parámetro lambda
// Definimos la clase User como ejemplo (añádela a la aplicación)
public class User
{
public string Name { get; set; }
public bool IsActive { get; set; }
}
// Método que acepta una lista y un delegado-condición (lambda)
public static List<User> FilterUsers(List<User> users, Predicate<User> predicate)
{
var result = new List<User>();
foreach (var user in users)
{
if (predicate(user)) // ¡Invocamos la lambda!
result.Add(user);
}
return result;
}
Ahora se puede pasar cualquier lambda:
var users = new List<User>
{
new User { Name = "Vasya", IsActive = true },
new User { Name = "Petya", IsActive = false },
new User { Name = "Masha", IsActive = true }
};
// Filtramos solo los activos
var activeUsers = FilterUsers(users, user => user.IsActive);
foreach (var user in activeUsers)
Console.WriteLine(user.Name); // Vasya, Masha
¡Y ya está! Pasamos un pedazo de lógica — una mini-función — como un parámetro normal, simplemente porque el método FilterUsers espera un Predicate<User>, y le dimos una lambda compatible.
Variante con Func<T, TResult>
Predicate<T> encaja cuando necesitas una condición (retorno bool). ¿Y si queremos “calcular” algo para cada usuario?
// Método que aplica una función a cada elemento y recoge los resultados
public static List<TResult> MapUsers<TResult>(List<User> users, Func<User, TResult> selector)
{
var result = new List<TResult>();
foreach (var user in users)
{
result.Add(selector(user));
}
return result;
}
Uso:
var names = MapUsers(users, user => user.Name.ToUpper());
foreach (var name in names)
Console.WriteLine(name); // VASYA, PETYA, MASHA
3. Matices útiles
Diferentes formas de pasar
Puedes pasar no solo una lambda, sino también un método normal — lo importante es que la firma coincida.
// Método normal
static bool NameHasS(User user) => user.Name.Contains("s");
// Pasando el método normal:
var usersWithS = FilterUsers(users, NameHasS);
// Pasando una lambda
var usersWithA = FilterUsers(users, u => u.Name.Contains("a"));
También puedes usar un método anónimo a la manera antigua (pero no lo recomiendo):
var usersWithM = FilterUsers(users, delegate(User u) { return u.Name.Contains("m"); });
El estilo moderno: ¡lambdas!
Pasar lambdas a LINQ: qué pasa en realidad
var result = users.Where(u => u.IsActive).ToList();
Por debajo, Where acepta Func<User, bool>. Eso significa que cualquier método que acepte Func<...> puede usarse igual.
¿Y si quieres dos parámetros?
// Método que acepta dos lambdas para filtrar
public static List<User> FilterUsersCustom(
List<User> users,
Func<User, bool> include,
Func<User, bool> exclude)
{
var result = new List<User>();
foreach (var user in users)
{
if (include(user) && !exclude(user))
result.Add(user);
}
return result;
}
Uso:
var customFiltered = FilterUsersCustom(
users,
u => u.Name.StartsWith("V"),
u => u.IsActive == false
);
// Tomará solo usuarios cuyo nombre empieza por "V" y que estén activos
Escenario: Fábrica de filtros
Console.WriteLine("Introduce la longitud mínima del nombre:");
int minLength = int.Parse(Console.ReadLine());
Predicate<User> lengthFilter = user => user.Name.Length >= minLength;
var filteredUsers = FilterUsers(users, lengthFilter);
// Bastante interactivo y vivo!
4. Errores típicos y matices
A veces el compilador no puede "inferir" el tipo de los parámetros de la lambda — especialmente en escenarios complejos con sobrecargas o cuando el método requiere un delegado con varios parámetros/retorno concreto. En ese caso puedes indicar explícitamente los tipos de la lambda:
FilterUsers(users, (User u) => u.Name.Length > 3);
o incluso:
MapUsers(users, (User u) => u.Name.ToUpper());
Error: la lambda no coincide con la firma
FilterUsers(users, user => Console.WriteLine(user.Name)); // ¡error! Se esperaba bool, se obtuvo void
Porque se espera una función que devuelva bool, y la lambda devuelve void (más precisamente, no devuelve nada). Presta atención al tipo de retorno.
Error: abuso de lambdas
Si empiezas a pasar lambdas de 10 líneas, mejor sácalas a un método separado. Así es más legible y depurarlo es más sencillo.
GO TO FULL VERSION