1. Wprowadzenie
Do tej pory widzieliście: wywoływaliśmy metody LINQ, które przyjmowały lambda jako parametr. Ale jak metoda w C# wie, że w ogóle można jej przekazać funkcję?
Mówiąc prosto, delegaty to jak interfejs dla funkcji: opisujemy sygnaturę (typy parametrów i typ zwracany), i dowolna metoda (albo lambda!), pasująca do tej sygnatury, może być przekazana tam, gdzie oczekiwany jest taki delegat.
Pamiętajcie, jak przekazywaliście string jako parametr? To mniej więcej tak samo działa z „logiką”, tylko typ parametru to delegat.
Delegaty: podstawowa teoria w przystępnej formie
W C# delegat to typ, który opisuje „funkcję o takiej a takiej sygnaturze”.
// Delegat, który przyjmuje int i zwraca bool
public delegate bool IntPredicate(int x);
Każda funkcja zgodna z sygnaturą może być przypisana do zmiennej tego typu:
bool IsEven(int n) => n % 2 == 0;
IntPredicate pred = IsEven;
A teraz — lambda też pasuje:
IntPredicate pred = x => x % 2 == 0;
Uniwersalne delegaty: Func, Action, Predicate
- Func<T1, ..., TResult> — funkcja przyjmująca parametry T1, ... i zwracająca TResult.
- Action<T1, ...> — funkcja przyjmująca parametry i nie zwracająca wartości (void).
- Predicate<T> — funkcja przyjmująca T i zwracająca bool.
2. Przekazywanie lambda do własnej metody
Wyobraźmy sobie, że rozwijamy nasze edukacyjne mini-aplikację — projekt konsolowy, która pracuje z listą użytkowników. Wcześniej filtrowaliśmy kolekcje przez LINQ, a teraz napiszemy własną metodę, która przyjmuje warunek jako lambda.
Tworzymy własną metodę z parametrem-lambdą
// Zdefiniujmy klasę User dla przykładu (dodajemy do naszej aplikacji)
public class User
{
public string Name { get; set; }
public bool IsActive { get; set; }
}
// Metoda, która przyjmuje listę i delegat-warunek (lambdę)
public static List<User> FilterUsers(List<User> users, Predicate<User> predicate)
{
var result = new List<User>();
foreach (var user in users)
{
if (predicate(user)) // Wywołujemy lambdę!
result.Add(user);
}
return result;
}
Teraz można przekazywać dowolną lambdę:
var users = new List<User>
{
new User { Name = "Alice", IsActive = true },
new User { Name = "Bob", IsActive = false },
new User { Name = "Charlie", IsActive = true }
};
// Filtrowanie tylko aktywnych
var activeUsers = FilterUsers(users, user => user.IsActive);
foreach (var user in activeUsers)
Console.WriteLine(user.Name); // Alice, Charlie
I tyle! Przekazaliśmy kawałek logiki — mini-funkcję — jako zwykły parametr, bo metoda FilterUsers oczekuje Predicate<User>, a daliśmy jej pasującą lambdę.
Wariant z Func<T, TResult>
Predicate<T> jest wygodny, kiedy potrzebujemy warunku (zwracamy bool). A jeśli chcemy coś „obliczyć” dla każdego użytkownika?
// Metoda, która stosuje funkcję do każdego elementu i zbiera wyniki
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;
}
Użycie:
var names = MapUsers(users, user => user.Name.ToUpper());
foreach (var name in names)
Console.WriteLine(name); // VASIA, PETIA, MASHA
3. Przydatne niuanse
Różne formy przekazywania
Można przekazać nie tylko lambdę, ale też zwykłą metodę — ważne, żeby sygnatura się zgadzała.
// Zwykła metoda
static bool NameHasS(User user) => user.Name.Contains("s");
// Przekazanie zwykłej metody:
var usersWithS = FilterUsers(users, NameHasS);
// Przekazanie lambdy
var usersWithA = FilterUsers(users, u => u.Name.Contains("a"));
Można też użyć anonimowej metody w starym stylu (ale lepiej tego unikać):
var usersWithM = FilterUsers(users, delegate(User u) { return u.Name.Contains("m"); });
Nowoczesny styl to lambdy!
Przekazywanie lambd do LINQ: co naprawdę się dzieje
var result = users.Where(u => u.IsActive).ToList();
Pod maską Where przyjmuje Func<User, bool>. To znaczy, że każda metoda, która oczekuje Func<...>, może być użyta w ten sam sposób!
A co, jeśli chcemy dwa parametry?
// Metoda przyjmuje dwie lambdy do filtrowania
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;
}
Użycie:
var customFiltered = FilterUsersCustom(
users,
u => u.Name.StartsWith("V"),
u => u.IsActive == false
);
// Weźmie tylko użytkowników, których imiona zaczynają się na "V" i którzy są aktywni
Scenariusz: Fabryka filtrów
Console.WriteLine("Wpisz minimalną długość imienia:");
int minLength = int.Parse(Console.ReadLine());
Predicate<User> lengthFilter = user => user.Name.Length >= minLength;
var filteredUsers = FilterUsers(users, lengthFilter);
// Całkiem interaktywnie i żywo!
4. Typowe błędy i niuanse
Czasem kompilator nie potrafi „wywnioskować” typów parametrów lambdy — szczególnie w złożonych scenariuszach z przeciążeniami albo gdy metoda wymaga delegata z kilkoma parametrami/konkretnego typu zwracanego. W takim przypadku można jawnie podać typy w lambdzie:
FilterUsers(users, (User u) => u.Name.Length > 3);
albo nawet:
MapUsers(users, (User u) => u.Name.ToUpper());
Błąd: lambda nie pasuje do sygnatury
FilterUsers(users, user => Console.WriteLine(user.Name)); // błąd! Oczekiwano bool, otrzymano void
Bo oczekiwana jest funkcja zwracająca bool, a lambda zwraca void (dokładniej — nic jawnie nie zwraca). Zwracaj uwagę na typ zwracany!
Błąd: nadużywanie lambd
Jeśli zaczynasz przekazywać lambdy mające 10 linii, lepiej wydziel je do osobnej metody. To czytelniejsze i łatwiej debugować.
GO TO FULL VERSION