Delegaty i zdarzenia — przekazywanie zachowania
Metoda też może być danymi: da się ją zapisać w zmiennej, przekazać jako argument i wywołać, nie wiedząc, która to metoda. Na tym stoi LINQ i cała obsługa okien.
Czego się dziś nauczysz
- Zapiszesz metodę w zmiennej typu delegata i wywołasz ją przez tę zmienną
- Dobierzesz
FuncalboActiondo kształtu metody - Zapiszesz wyrażenie lambda w postaci krótkiej i w klamrach
- Napiszesz metodę przyjmującą zachowanie jako parametr
- Ogłosisz zdarzenie i zapiszesz do niego kilka reakcji
Przygotowanie: lekcje 01–52. Przewidywany czas: 45–90 minut z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.
Zmienna, w której siedzi metoda
Do tej pory zmienna przechowywała dane: liczbę, napis, obiekt. Delegat to typ zmiennej, która przechowuje metodę — i pozwala ją wywołać, nie wiedząc, która to metoda.
delegate int Operacja(int a, int b); // deklaracja typu delegata
static int Dodaj(int a, int b) => a + b;
static int Pomnoz(int a, int b) => a * b;
Operacja dzialanie = Dodaj;
Console.WriteLine(dzialanie(3, 4)); // 7
dzialanie = Pomnoz; // ta sama zmienna, inna metoda
Console.WriteLine(dzialanie(3, 4)); // 12
Deklaracja delegata mówi tylko o kształcie metody: jakie przyjmuje parametry i co zwraca. Pasuje do niej każda metoda o takim kształcie — własna, statyczna, z obiektu.
Gotowe typy: Func i Action
W praktyce rzadko deklaruje się własny delegat. Biblioteka .NET ma dwa gotowe zestawy:
| Zapis | Znaczenie |
|---|---|
Action | metoda bez parametrów, nic nie zwraca |
Action<int> | przyjmuje int, nic nie zwraca |
Action<string, int> | przyjmuje dwa parametry, nic nie zwraca |
Func<int> | bez parametrów, zwraca int |
Func<int, bool> | przyjmuje int, zwraca bool — ostatni typ to zawsze wynik |
Func<int, int, int> | przyjmuje dwa int, zwraca int |
Func<int, int, int> dzialanie = Dodaj;
Action<string> pokaz = Console.WriteLine;
pokaz($"wynik: {dzialanie(3, 4)}");
Lambda — metoda bez nazwy
Jeśli metoda ma być użyta w jednym miejscu, nie warto nadawać jej nazwy. Wyrażenie lambda zapisuje ją w miejscu użycia:
Func<int, int, int> dodaj = (a, b) => a + b;
Func<int, bool> parzysta = x => x % 2 == 0;
Action<string> powitaj = imie => Console.WriteLine($"Czesc, {imie}");
Console.WriteLine(dodaj(3, 4)); // 7
Console.WriteLine(parzysta(10)); // True
powitaj("Ala"); // Czesc, Ala
Strzałka => oddziela parametry od ciała. Typy parametrów są wywnioskowane z typu delegata, więc nie trzeba ich pisać. Gdy ciało ma więcej niż jedno wyrażenie, obejmujemy je klamrami i używamy return:
Func<int, string> ocena = punkty =>
{
if (punkty >= 90) return "bardzo dobry";
if (punkty >= 50) return "dostateczny";
return "niedostateczny";
};
Po co to wszystko: metoda jako argument
Prawdziwy zysk pojawia się, gdy metoda przyjmuje zachowanie jako parametr. Jedna metoda filtrująca zamiast dziesięciu:
static List<int> Filtruj(int[] dane, Func<int, bool> warunek)
{
List<int> wynik = new List<int>();
foreach (int x in dane)
{
if (warunek(x)) wynik.Add(x);
}
return wynik;
}
int[] punkty = { 12, 4, 18, 10, 7 };
var duze = Filtruj(punkty, x => x >= 10);
var parzyste = Filtruj(punkty, x => x % 2 == 0);
var jedno = Filtruj(punkty, x => x == 7);
To jest fundament LINQ
Metody Where, Select i OrderBy z następnej lekcji to dokładnie takie metody: przyjmują Func i wywołują je dla kolejnych elementów. Gdy zrozumiesz Filtruj, zrozumiesz LINQ.
Zdarzenia — powiadamianie zainteresowanych
Klasa, która coś obserwuje — licznik, czujnik, przycisk — nie powinna wiedzieć, co ma się stać po zmianie. Ona tylko ogłasza, że coś się wydarzyło. Kto chce, ten się zapisuje.
class Magazyn
{
private int stan;
private readonly int prog;
public event Action<int>? StanPonizejProgu; // zdarzenie
public Magazyn(int stanPoczatkowy, int prog)
{
stan = stanPoczatkowy;
this.prog = prog;
}
public void Wydaj(int ile)
{
stan -= ile;
if (stan < prog)
{
StanPonizejProgu?.Invoke(stan); // ogloszenie
}
}
}
Magazyn m = new Magazyn(10, 5);
m.StanPonizejProgu += stan => Console.WriteLine($"UWAGA: zostalo {stan} sztuk");
m.StanPonizejProgu += stan => Console.WriteLine("Wyslano zamowienie do dostawcy");
m.Wydaj(3); // nic - stan 7
m.Wydaj(3); // oba komunikaty - stan 4
UWAGA: zostalo 4 sztuk
Wyslano zamowienie do dostawcy
eventprzed typem delegata ogranicza to, co wolno robić z zewnątrz: można się zapisać (+=) i wypisać (-=), ale nie da się podmienić całej listy ani wywołać zdarzenia spoza klasy.- Zapisanych metod może być wiele; wywołanie uruchamia je wszystkie w kolejności zapisania.
?.Invoke(...)chroni przed wyjątkiem, gdy nikt się nie zapisał — zdarzenie bez subskrybentów jestnull.
Tu zaczyna się aplikacja okienkowa
W WPF i WinForms cały interfejs działa na zdarzeniach: przycisk.Click += ..., okno.Loaded += .... Kod, który napiszesz w oknie edytora graficznego, to metoda zapisana do zdarzenia kontrolki. Ta lekcja to przygotowanie do programowania aplikacji desktopowych.
Przykład: kalkulator sterowany delegatami
using System.Collections.Generic;
class Program
{
static void Main()
{
Dictionary<string, Func<double, double, double>> dzialania =
new Dictionary<string, Func<double, double, double>>
{
["+"] = (a, b) => a + b,
["-"] = (a, b) => a - b,
["*"] = (a, b) => a * b,
["/"] = (a, b) => b == 0 ? double.NaN : a / b
};
double x = 12, y = 5;
foreach (KeyValuePair<string, Func<double, double, double>> para in dzialania)
{
Console.WriteLine($"{x} {para.Key} {y} = {para.Value(x, y):F2}");
}
Console.Write("Podaj znak dzialania: ");
string znak = Console.ReadLine() ?? "";
if (dzialania.TryGetValue(znak, out Func<double, double, double>? wybrane))
{
Console.WriteLine($"Wynik: {wybrane(x, y):F2}");
}
else
{
Console.WriteLine("Nieznane dzialanie.");
}
}
}
12 + 5 = 17,00
12 - 5 = 7,00
12 * 5 = 60,00
12 / 5 = 2,40
Podaj znak dzialania: *
Wynik: 60,00
Co dzieje się po kolei
- Słownik przechowuje nie liczby, tylko działania. Dodanie potęgowania to jeden wiersz — bez ruszania
switchi bez zmieniania pętli wypisującej. para.Value(x, y)wywołuje metodę zapisaną pod danym kluczem.- Dzielenie przez zero zwraca
double.NaNzamiast zgłaszać wyjątek — dla typudoubleto wartość dozwolona (lekcja 06). Sprawdzisz ją metodądouble.IsFinite. - Porównaj to z zadaniem „menu programu” z lekcji 15: tam każda opcja wymagała nowej gałęzi
switch.
Zestawienie elementów
| Element | Znaczenie | Uwagi |
|---|---|---|
delegate W Nazwa(...) | własny typ delegata | Opisuje kształt metody. W praktyce zwykle wystarczą Func i Action. |
Func<A, B, W> | metoda zwracająca wynik | Ostatni parametr typu to typ wyniku. |
Action<A> | metoda bez wyniku | Odpowiednik void. |
Predicate<T> | warunek | To samo co Func<T, bool>; używany m.in. przez List<T>.Find i RemoveAll. |
(a, b) => wyrażenie | lambda | Metoda bez nazwy. Typy parametrów wynikają z typu delegata. |
event | modyfikator | Ogranicza dostęp z zewnątrz do += i -=. Wywołać można tylko wewnątrz klasy. |
zdarzenie?.Invoke(x) | wywołanie | Uruchamia wszystkie zapisane metody. Operator ?. chroni przed null. |
+= / -= na zdarzeniu | zapis / wypis | Ta sama metoda zapisana dwa razy wykona się dwa razy. Wypisz się, gdy obiekt przestaje być potrzebny. |
lista.Find(warunek) | T? | Przykład metody biblioteki przyjmującej delegat. Zwraca pierwszy pasujący element albo default. |
Na co uważać
| Zapis | Problem |
|---|---|
Operacja d = Dodaj(); | Nawiasy wywołują metodę. Do delegata przypisujemy nazwę: = Dodaj;. |
Func<int, int> dla metody void | Func zawsze zwraca wynik. Dla metod bez wyniku jest Action. |
zdarzenie.Invoke(x) bez ?. | NullReferenceException, gdy nikt się nie zapisał. |
| Wywołanie zdarzenia spoza klasy | Błąd kompilacji — i słusznie: ogłasza tylko właściciel zdarzenia. |
Brak -= przy długo żyjących obiektach | Obiekt zapisany do zdarzenia nie zostanie zwolniony z pamięci, dopóki źródło zdarzenia istnieje. |
Lambda z { } bez return | Ciało w klamrach musi zwrócić wartość jawnie, jeśli delegat jej oczekuje. |
Zadania
Zadeklaruj Func<double, double, double> i przypisz do niej kolejno dodawanie, odejmowanie i potęgowanie zapisane jako lambdy. Po każdym przypisaniu wypisz wynik dla tych samych argumentów.
Napisz metodę PokazWszystko(int[] dane, Action<int> sposob). Wywołaj ją raz ze zwykłym wypisaniem, a raz z wypisaniem w nawiasach kwadratowych.
Napisz metodę Filtruj z przykładu i użyj jej do wybrania: liczb dodatnich, liczb trzycyfrowych i liczb podzielnych przez 7. Nie zmieniaj samej metody.
Napisz metodę Mapuj<T, W>(T[] dane, Func<T, W> przeksztalcenie), zwracającą nową tablicę. Użyj jej do zamiany liczb na ich kwadraty oraz imion na ich długości.
Zbuduj klasę Czujnik ze zdarzeniem PrzekroczonoProg. Zapisz do niego dwie reakcje: wypisanie ostrzeżenia i zwiększenie licznika alarmów. Sprawdź, że obie działają, i wypisz licznik na końcu.
Napisz program z menu tekstowym, w którym opcje trzymane są w Dictionary<string, Action>: kluczem jest tekst wpisywany przez użytkownika, a wartością czynność do wykonania.
Zacznij od trzech opcji — powitanie, aktualna data i wyczyszczenie ekranu — oraz opcji kończącej program. Pętla obsługująca menu ma wyglądać tak samo niezależnie od liczby opcji: odczytaj wybór, znajdź go w słowniku metodą TryGetValue, wywołaj albo zgłoś nieznaną opcję.
Kryterium sukcesu: dodanie czwartej opcji wymaga dopisania jednego wpisu do słownika i żadnej zmiany w pętli.
Co trzeba zapamiętać
- Delegat to typ zmiennej przechowującej metodę o określonym kształcie.
Funczwraca wynik (ostatni parametr typu),Actionnie zwraca nic.- Lambda
(a, b) => …to metoda bez nazwy, zapisana w miejscu użycia. - Metoda przyjmująca delegat pozwala oddzielić co robimy od jak przechodzimy dane — na tym opiera się LINQ.
eventpozwala z zewnątrz tylko zapisać się i wypisać; ogłasza wyłącznie właściciel zdarzenia.- Zdarzenie bez subskrybentów jest
null— wywołuj je przez?.Invoke(...).
Dokumentacja: Microsoft Learn — temat tej lekcji.