1. Introdução
Até agora você viu: chamávamos métodos do LINQ que recebiam uma lambda. Mas como o método em C# sabe que dá pra passar uma função pra ele?
De forma simples, delegates são como uma interface para funções: descrevemos a assinatura (tipos dos parâmetros e tipo de retorno), e qualquer método (ou lambda!) que combinar com essa assinatura pode ser passado onde esse delegate é esperado.
Lembra como você passava uma string como parâmetro? Funciona mais ou menos do mesmo jeito com “lógica”, só que o tipo do parâmetro é um delegate.
Delegates: teoria básica numa linguagem simples
Em C# delegate é um tipo que descreve “uma função com determinada assinatura”.
// Delegate que recebe int e retorna bool
public delegate bool IntPredicate(int x);
Qualquer função compatível com a assinatura pode ser atribuída a uma variável desse tipo:
bool IsEven(int n) => n % 2 == 0;
IntPredicate pred = IsEven;
E agora — uma lambda também serve:
IntPredicate pred = x => x % 2 == 0;
Delegates genéricos: Func, Action, Predicate
- Func<T1, ..., TResult> — função que recebe parâmetros T1, ... e retorna TResult.
- Action<T1, ...> — função que recebe parâmetros e não retorna valor (void).
- Predicate<T> — função que recebe T e retorna bool.
2. Passando uma lambda para seu método
Vamos imaginar que estamos expandindo nosso mini-app de estudo — um projeto console que trabalha com uma lista de usuários. Antes a gente filtrava coleções via LINQ, agora vamos escrever nosso próprio método que recebe uma condição em forma de lambda.
Criando um método com parâmetro lambda
// Definimos a classe User como exemplo (adicionar ao app)
public class User
{
public string Name { get; set; }
public bool IsActive { get; set; }
}
// Método que recebe a lista e um delegate-condição (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)) // Chamamos a lambda!
result.Add(user);
}
return result;
}
Agora dá pra passar qualquer 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 apenas os ativos
var activeUsers = FilterUsers(users, user => user.IsActive);
foreach (var user in activeUsers)
Console.WriteLine(user.Name); // Vasya, Masha
É isso! Passamos um pedaço de lógica — uma mini-função — como parâmetro normal, só porque o método FilterUsers espera um Predicate<User>, e nós demos uma lambda compatível.
Variação com Func<T, TResult>
Predicate<T> serve quando você precisa de uma condição (retorna bool). E se quisermos “calcular” algo para cada usuário?
// Método que aplica uma função a cada elemento e coleta 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. Dicas úteis
Diferentes formas de passar
Você pode passar não só uma lambda, mas também um método normal — o importante é que a assinatura bata.
// Método normal
static bool NameHasS(User user) => user.Name.Contains("s");
// Passando o método normal:
var usersWithS = FilterUsers(users, NameHasS);
// Passando uma lambda
var usersWithA = FilterUsers(users, u => u.Name.Contains("a"));
Também dá pra usar um método anônimo antigo (mas não recomendo):
var usersWithM = FilterUsers(users, delegate(User u) { return u.Name.Contains("m"); });
Estilo moderno = lambdas!
Passando lambdas para LINQ: o que realmente acontece
var result = users.Where(u => u.IsActive).ToList();
Por baixo do capô o Where aceita um Func<User, bool>. Isso quer dizer que qualquer método que aceite Func<...> pode ser usado da mesma forma!
E se precisar de dois parâmetros?
// Método aceita duas lambdas pra filtragem
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
);
// Vai pegar só usuários cujos nomes começam com "V" e que são ativos
Cenário: Fábrica de filtros
Console.WriteLine("Digite o comprimento mínimo do nome:");
int minLength = int.Parse(Console.ReadLine());
Predicate<User> lengthFilter = user => user.Name.Length >= minLength;
var filteredUsers = FilterUsers(users, lengthFilter);
// Bem interativo e dinâmico!
4. Erros típicos e sutilezas
Às vezes o compilador não consegue "inferir" os tipos dos parâmetros da lambda — especialmente em cenários complexos com overloads ou quando o método exige um delegate com vários parâmetros/tipo de retorno específico. Nesse caso você pode especificar explicitamente os tipos na lambda:
FilterUsers(users, (User u) => u.Name.Length > 3);
ou mesmo:
MapUsers(users, (User u) => u.Name.ToUpper());
Erro: lambda não corresponde à assinatura
FilterUsers(users, user => Console.WriteLine(user.Name)); // erro! Esperava-se bool, recebeu-se void
Isso acontece porque se espera uma função que retorne bool, mas a lambda retorna void (na verdade, não retorna nada). Preste atenção ao tipo de retorno!
Erro: abuso de lambdas
Se você começa a passar lambdas de 10 linhas, melhor extrair pra um método separado. Fica mais legível e mais fácil de debugar.
GO TO FULL VERSION