1. Wprowadzenie
W wielu nowoczesnych projektach C# prawie nie używa się "zwykłych" metod do obsługi zdarzeń. Tak wyszło, ponieważ wyrażenia lambda pozwalają szybko i zwięźle zadeklarować handler bezpośrednio w miejscu subskrypcji (operator +=), jeśli obsługa jest prosta i nie jest ponownie wykorzystywana w innych częściach kodu. To przypomina przyklejenie małej notatki bezpośrednio na ekspresie do kawy z instrukcją „naciśnij ten przycisk” zamiast pisania szczegółowego manuala i trzymania go w osobnym folderze. Jeśli zadanie jest lokalne i jednorazowe, lambda jest idealna!
Gdzie to jest używane w praktyce
- W ASP.NET (na przykład przy obsłudze zdarzeń cyklu życia strony),
- W WPF/WinForms dla UI (na przykład przy kliknięciu przycisku),
- W programowaniu serwerowym (na przykład logika w pipeline),
- W testach, kiedy handlerowi nie trzeba oddzielnej nazwy.
Składnia: jak to wygląda w kodzie
Na początek rozważmy zwykłą obsługę zdarzenia:
// Obwieszczenie zdarzenia
public event EventHandler? MyEvent;
// Subskrypcja zdarzenia zwykłą metodą
void Handler(object? sender, EventArgs e)
{
Console.WriteLine("Event occurred!");
}
public void Subscribe()
{
MyEvent += Handler;
}
Teraz — wariant z wyrażeniem lambda (funkcja anonimowa):
public void Subscribe()
{
MyEvent += (sender, e) => Console.WriteLine("Event occurred (lambda)!");
}
Zwróć uwagę: nie tworzymy oddzielnej metody, tylko ustawiamy handler od razu przy subskrypcji zdarzenia. Sygnatura lambdy automatycznie dopasowuje się do typu zdarzenia (EventHandler).
Porównanie podejść
| Zwykła metoda | Lambda | |
|---|---|---|
| Objętość kodu | Więcej (metoda + subskrypcja) | Mniej, wszystko w miejscu |
| Możliwość ponownego użycia | Można ponownie użyć | Zazwyczaj nie |
| Lokalność kodu | Rozrzucone | Wszystko obok |
| Jasność/czytelność | Dobre dla skomplikowanej logiki | Świetne dla prostych przypadków |
2. Aplikacja z obsługą zdarzeń i lambdami
Podstawowa struktura
public class Menu
{
public event EventHandler? ItemSelected;
public void SelectItem(int index)
{
Console.WriteLine($"Punkt menu {index} wybrany.");
ItemSelected?.Invoke(this, EventArgs.Empty);
}
}
Subskrypcja z wyrażeniem lambda
class Program
{
static void Main()
{
var menu = new Menu();
// Subskrypcja zdarzenia przez lambdę
menu.ItemSelected += (sender, e) =>
{
Console.WriteLine("Dziękuję za wybór! Lambda-handler się uruchomił.");
};
menu.SelectItem(1);
}
}
Oczekiwane wyjście:
Punkt menu 1 wybrany.
Dziękuję za wybór! Lambda-handler się uruchomił.
Przechwytywanie zmiennych z zewnętrznego kontekstu
Jedną z zalet wyrażenia lambda jest to, że może "zapamiętywać" wartości z zewnętrznego scope'u (obszaru widoczności zmiennych). Na przykład liczyć, ile razy wybrano punkt menu: zmienna counter będzie przechwycona przez zamknięcie.
static void Main()
{
var menu = new Menu();
int counter = 0;
menu.ItemSelected += (s, e) =>
{
counter++;
Console.WriteLine($"Punkt wybrany {counter} raz(y)!");
};
menu.SelectItem(1);
menu.SelectItem(2);
}
Oczekiwane wyjście:
Punkt menu 1 wybrany.
Punkt wybrany 1 raz(y)!
Punkt menu 2 wybrany.
Punkt wybrany 2 raz(y)!
To magia zamknięć w praktyce: zmienna counter nadal żyje wewnątrz lambdy!
Przykład z parametrami zdarzenia
Jeśli Twoje zdarzenie używa EventHandler<T>, gdzie T — własna klasa z dodatkowymi informacjami, wyrażenie lambda po prostu "dopasowuje się" do potrzebnej sygnatury.
public class MenuItemSelectedEventArgs : EventArgs
{
public int ItemIndex { get; }
public string Description { get; }
public MenuItemSelectedEventArgs(int itemIndex, string description)
{
ItemIndex = itemIndex;
Description = description;
}
}
public class Menu
{
public event EventHandler<MenuItemSelectedEventArgs>? ItemSelected;
public void SelectItem(int index, string description)
{
Console.WriteLine($"Punkt menu {index}: {description} wybrany.");
ItemSelected?.Invoke(this, new MenuItemSelectedEventArgs(index, description));
}
}
// Użycie
static void Main()
{
var menu = new Menu();
// Lambda z rozpakowaniem argumentów zdarzenia
menu.ItemSelected += (sender, args) =>
{
Console.WriteLine($"Wybrano punkt #{args.ItemIndex}: {args.Description.ToUpper()}");
};
menu.SelectItem(3, "About");
}
Oczekiwane wyjście:
Punkt menu 3: About wybrany.
Wybrano punkt #3: O PROGRAME
3. Lokalni handlerzy i lambda
Lambdy są idealne, kiedy:
- Logika obsługi jest krótka i jasna,
- Handler używany jest tylko w jednym miejscu,
- Trzeba "zamknąć" zmienne z lokalnego kontekstu.
Jeśli obsługa jest skomplikowana, wymaga ponownego użycia lub może być wywoływana poza miejscem deklaracji, lepiej użyć oddzielnej nazwanej metody.
Przykład: logika wewnątrz i na zewnątrz
Lambda (idealnie):
button.Click += (s, e) => MessageBox.Show("Button is pressed!");
Metody (gdy logika jest bardziej skomplikowana lub potrzebna wielokrotnie):
button.Click += Button_Click;
void Button_Click(object sender, EventArgs e)
{
if (UserConfirmed())
{
SaveData();
MessageBox.Show("Data saved!");
}
}
4. Pod maską: co się dzieje z lambda-handlerami
Lambda to też delegate, jak zwykły handler. Kompilator „w locie” tworzy anonimową metodę, a jeśli w niej są przechwycone zmienne, to także ukrytą klasę do ich przechowywania.
Ważne pamiętać (i to częsty błąd): jeśli tworzysz lambdę w pętli i subskrybujesz ją na zdarzenie, wszystkie iteracje mogą przechwycić tę samą zmienną pętli.
for (int i = 0; i < 5; i++)
{
buttons[i].Click += (sender, e) =>
{
Console.WriteLine($"Click the button #{i}");
};
}
Dla wszystkich handlerów numer przycisku może się okazać równy 5! Aby temu zapobiec, stwórz kopię zmiennej wewnątrz pętli:
for (int i = 0; i < 5; i++)
{
int buttonIndex = i; // Lokalna kopia
buttons[i].Click += (sender, e) =>
{
Console.WriteLine($"Click the button #{buttonIndex}");
};
}
Teraz wszystko działa jak oczekiwano.
Cała moc lambda-handlerów: życie w praktyce
Takie wyrażenia lambda oszczędzają czas i przyspieszają development, szczególnie gdy logika jest prosta. Sprawiają, że kod jest "bliżej problemu", nie rozrzucając go po różnych plikach i klasach. W realnych projektach spotkasz je wszędzie: od obsługi zdarzeń w UI po subskrypcje wiadomości w asynchronicznych szynach zdarzeń.
5. Typowe błędy
Błąd nr 1: zapomnieć o odsubskrybowaniu (-=) przy obiektach długowiecznych.
Jeśli obiekt-subskrybent nie odsubskrybuje się od zdarzenia wydawcy, referencja do delegate trzyma subskrybenta w pamięci — garbage collector nie usunie go, nawet jeśli nie ma innych referencji. W rezultacie są "wycieki" pamięci i wiszące zależności, zwłaszcza gdy wydawca żyje długo (zdarzenia statyczne, singletony, serwisy).
Jak unikać: zawsze odsubskrybuj podczas zwalniania/niszczenia (np. Dispose, OnDisable, OnDestroy). Rozważ słabe referencje (weak events / WeakEventManager), użycie wzorca „event manager” lub IObservable/Rx, jeśli scenariusz jest skomplikowany.
Błąd nr 2: przechwytywanie zmiennych z pętli bez lokalnej kopii.
Typowa pułapka — pisać subskrypcję w pętli i zamknięcie chwytające tę samą zmienną pętli, przez co wszystkie handlery widzą jej finalną wartość, a nie tę, jaka była przy tworzeniu handlera. To prowadzi do nieoczekiwanych rezultatów (wszystkie handlery "wypisują" tę samą liczbę itp.).
Jak unikać: wewnątrz pętli twórz lokalną kopię wartości i chwytaj ją:
for (int i = 0; i < n; i++)
{
int current = i;
button.Click += (s, e) => Handle(current);
}
Albo przekazuj potrzebną wartość do metody-opakowania. To bardziej niezawodne i czytelne.
Błąd nr 3: używanie ogromnych lambd w miejscu subskrypcji.
Jeśli wkładasz dużą logikę biznesową bezpośrednio w funkcję anonimową przy subskrypcji, kod staje się trudny do czytania, trudno go testować i debugować. Dodatkowo — przy anonimowych lambdach trudniej poprawnie się odsubskrybować, bo potrzebujesz tej samej instancji delegate. W rezultacie logika rozprasza się po kodzie i traci strukturę.
Jak unikać: wyciągaj skomplikowaną logikę do nazwanych metod lub serwisów; jeśli trzeba, przechowuj delegate w zmiennej/polu, żeby móc się odsubskrybować; zostaw w lambdzie tylko małe opakowanie/przekierowanie. To poprawia czytelność i zarządzalność kodu.
GO TO FULL VERSION