CodeGym /Kursy /C# SELF /Obsługa zdarzeń: wyrażenia lambda

Obsługa zdarzeń: wyrażenia lambda

C# SELF
Poziom 52, Lekcja 4
Dostępny

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.

2
Zadanie
C# SELF, poziom 52, lekcja 4
Niedostępne
Przycisk z danymi użytkownika
Przycisk z danymi użytkownika
1
Ankieta/quiz
Zdarzenia w C#, poziom 52, lekcja 4
Niedostępny
Zdarzenia w C#
Tworzenie zdarzeń za pomocą delegatów
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION