CodeGym /Kursy /C# SELF /Wyrażenia lambda z Func

Wyrażenia lambda z Func, Action, Predicate

C# SELF
Poziom 50 , Lekcja 1
Dostępny

1. Wprowadzenie

Wiesz co jest ciekawe? Wyrażenie lambda samo w sobie — to jak przepis bez garnka. Opisuje, co zrobić, ale potrzebuje „pojemnika”, żeby żyć w kodzie. W C# do tego są gotowe uniwersalne typy delegatów: Func, Action i Predicate.

Wyobraź sobie je jako gotowe formy dla twoich lambd — bierzesz odpowiednią i wlewasz swoją logikę. Żadnych ręcznie składanych wynalazków i własnych typów delegatów, gdy pod ręką jest wszystko, czego potrzeba.

Trochę historii

Na początku, gdy pojawiała się potrzeba przekazać funkcję jako parametr, trzeba było deklarować własny typ delegata. To było długo, uciążliwe i wyglądało tak:

delegate int Calculate(int x, int y);

Calculate adder = (a, b) => a + b;

Kiedy w C# pojawiły się Func, Action i Predicate, można było zapomnieć o ręcznym deklarowaniu delegatów w ~90% przypadków. Wygląda teraz dużo prościej i bardziej uniwersalnie:

Func<int, int, int> adder = (a, b) => a + b;

2. Func<T, TResult> — funkcja z wartością zwracaną

Składnia i przeznaczenie

Func — to generyczny delegat (generic delegate), który przyjmuje od 0 do 16 parametrów i zwraca wartość.

Func<int, int, int> sum = (x, y) => x + y;

Func<typ1, typ2, ..., typN, TResult> — wszystkie parametry oprócz ostatniego to argumenty, ostatni to typ zwracany.

Przykłady

1. Suma dwóch liczb

Func<int, int, int> sum = (x, y) => x + y;
Console.WriteLine(sum(3, 5)); // 8

2. Podnoszenie do kwadratu

Func<int, int> square = x => x * x;
Console.WriteLine(square(4)); // 16

3. Brak parametrów

Func<string> greet = () => "Hello, lambda!";
Console.WriteLine(greet());

Wizualny schemat

Sygnatura Przykład Opis
Func<int, int>
x => x * 2
Przyjmuje int, zwraca int
Func<int, int, int>
(a, b) => a + b
Dwa int, zwraca int
Func<string>
() => "hi"
Nie przyjmuje nic, zwraca string

Jak to wygląda w twojej aplikacji

Powiedzmy, że w naszym mini-aplikacji (warunkowy "Katalog użytkowników") mamy listę liczb — chcemy zastosować do niej różne przetworzenia. Na przykład podnieść do kwadratu albo policzyć sumę z ustaloną liczbą:

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> — akcja bez zwrotu

Action — uniwersalny delegat do metod, które coś robią (np. wypisują na ekran), ale nic nie zwracają.

Może przyjmować od 0 do 16 parametrów, ale zawsze zwraca void.

Przykłady

1. Wypisanie na ekran

Action<string> print = text => Console.WriteLine("Data: " + text);
print("Hello, world!");

2. Akcja bez parametrów

Action greet = () => Console.WriteLine("Welcome!");
greet();

3. Akcja z wieloma parametrami

Action<int, int> showSum = (a, b) => Console.WriteLine($"Sum: {a + b}");
showSum(2, 3); // Sum: 5

Wizualny schemat

Sygnatura Przykład Opis
Action
() => ...
Bez parametrów, bez zwrotu
Action<int>
x => ...
Jeden parametr
Action<int, string>
(x, s) => ...
Wiele parametrów

W naszej aplikacji

Dodajmy do katalogu użytkowników metodę do wypisania wszystkich imion:

List<string> names = new() { "Anna", "Boris", "Vika" };
Action<string> printName = name => Console.WriteLine("User: " + name);

names.ForEach(printName);
// albo tak: names.ForEach(name => Console.WriteLine(name));

4. Predicate<T> — tak czy nie?

Gdy potrzebna jest funkcja, która zwróci tylko true albo false dla jednego parametru, użyj Predicate<T>. To po prostu delegat, który przyjmuje jeden parametr i zwraca bool.

Predicate — oficjalne "potrzebujemy sprawdzenia boolean" opakowanie dla lambdy.

Przykłady

1. Sprawdzić, czy liczba jest większa niż 5

Predicate<int> isGreaterThanFive = x => x > 5;

Console.WriteLine(isGreaterThanFive(3)); // false
Console.WriteLine(isGreaterThanFive(7)); // true

2. Użycie z metodą List<T>.Find

List<int> values = new() { 2, 4, 7, 10 };
int found = values.Find(isGreaterThanFive); // używa Predicate<int>
Console.WriteLine(found); // 7

3. Czy wszyscy dorośli?

List<int> ages = new() { 12, 19, 34 };

bool allAdults = ages.TrueForAll(age => age >= 18);
// TrueForAll przyjmuje Predicate<int>

Czym różni się od Func<T, bool>?

W praktyce są w dużej mierze wymienne. Nawet dokumentacja Microsoft mówi: "Predicate<T> — to po prostu Func<T, bool> dla specjalnych API". Ale niektóre metody standardowej biblioteki czasami wymagają właśnie Predicate.

5. Jak lambdy "wpisują się" w Func, Action, Predicate

Kiedy piszesz lambdę, C# analizuje: "Aha, jej forma pasuje do wymaganego delegata — można podstawić!"

Func<int, int> f1 = x => x * 2;
Action<string> a1 = text => Console.WriteLine(text);
Predicate<int> p1 = x => x < 10;

Wszędzie lambda! Ale pod maską — trzy różne delegaty z różnymi sygnaturami.

Zastosowanie na przykładzie "rzeczywistego" kodu

List<User> users = new() {
    new User("Anna", 24),
    new User("Boris", 17),
    new User("Vika", 31),
};

// Funkcja zwracająca tylko dorosłych użytkowników (Predicate<User>)
List<User> adults = users.FindAll(user => user.Age >= 18);
Console.WriteLine("Spis dorosłych: " + string.Join(", ", adults.Select(u => u.Name)));

A jeśli chcemy wypisać imiona wszystkich użytkowników przez Action<User>:

users.ForEach(user => Console.WriteLine(user.Name));

Aby dostać ich imiona (Func<User, string>):

IEnumerable<string> names = users.Select(user => user.Name);

Tabela dla przejrzystości

Delegat Sygnatura Przykład lambdy Gdzie używany
Func<T, U>
T → U
user => user.Name
Select, wszelkie transformacje
Action<T>
T → void
user => Console.WriteLine(...)
ForEach, metody-akcje
Predicate<T>
T → bool
user => user.Age > 18
Find, Exists, filtry

6. Przykłady z wewnętrzną aplikacją: krok po kroku

Rozszerzmy naszą małą aplikację "Katalog użytkowników". Niech będzie klasa 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> — sprawdzamy, czy użytkownik jest pełnoletni

Func<User, bool> isAdult = user => user.Age >= 18;

Używamy w LINQ:

var adults = users.Where(isAdult);

2. Predicate<User> — szukamy aktywnego użytkownika

Predicate<User> isActive = user => user.IsActive;
User found = users.Find(isActive);

3. Action<User> — dezaktywujemy użytkownika

Action<User> deactivate = user => user.IsActive = false;
users.ForEach(deactivate);

4. Func<User, string> — dostajemy krótkie opisanie

Func<User, string> describe = user => $"{user.Name} ({user.Age})";
var descriptions = users.Select(describe);

Wszystkie te lambdy — to całkiem realny "kod w postaci danych", który można przekazywać do metod, przechowywać w zmiennych, łączyć.

7. Nieoczywiste niuanse i typowe błędy

1. Lambda musi pasować do sygnatury delegata. Jeśli sygnatura nie zgadza się, będzie błąd kompilacji.

Func<int, string> wrong = x => x * 2; // błąd: oczekiwany string, otrzymano int
// Poprawnie:
Func<int, string> right = x => (x * 2).ToString();

2. Nie zapominaj o void vs return. Action nie zwraca wartości — próba napisania czegoś takiego: Action<int> a = x => x * x; nie zadziała, bo zwracana jest wartość, choć nie powinna być.

3. Predicate<T> i Func<T, bool> często są wymienne, ale nie zawsze. Czasem metody kolekcji oczekują dokładnie Predicate<T>, czasem — Func<T, bool>. Bezpośrednie przypisanie może nie działać bez jawnego opakowania.

Predicate<int> pred = x => x > 0;
Func<int, bool> func = pred; // błąd
// Ale:
Func<int, bool> func2 = x => x > 0;
Predicate<int> pred2 = new Predicate<int>(func2); // można przekonwertować przez konstruktor
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION