CodeGym /Kursy /C# SELF /Przekazywanie wyrażeń lambda (

Przekazywanie wyrażeń lambda ( =>) jako parametr

C# SELF
Poziom 50 , Lekcja 2
Dostępny

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ć.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION