Wyrażenia lambda, delegaty i zdarzenia

Wskazówka

Dopiero zaczynasz programować oprogramowanie? Najpierw zacznij od samouczków Wprowadzenie . Przed użyciem wyrażeń lambda należy rozwijać podstawowe umiejętności dotyczące typów i metod.

Czasami chcesz przekazać mały element zachowania, funkcję bezpośrednio do innej metody. Możesz na przykład filtrować listę, ale warunek filtrowania zmienia się w zależności od sytuacji. Zamiast pisać oddzielną metodę nazwaną dla każdego możliwego warunku, należy przekazać sam warunek jako argument.

Wyrażenia lambda to funkcja języka C#, która umożliwia to. Wyrażenie lambda jest kompaktową funkcją śródliniową, którą piszesz bez podawania jej nazwy. Użyj operatora => strzałki, aby oddzielić listę parametrów od treści:

x => x * 2

Odczyt od lewej do prawej: x jest parametrem wejściowym, => oznacza "przechodzi do", i x * 2 jest treścią. Oblicza zwracaną wartość. Jeśli nie ma parametrów lub więcej niż jednego, zawijaj je w nawiasy: () => 42 lub (left, right) => left + right.

Delegaty obsługują wyrażenia lambda

Aby użyć wyrażenia lambda, kompilator języka C# musi znać dwie rzeczy: typy parametrów i typ zwracany. Ten opis, typy parametrów i typ zwracany, jest nazywany typem delegata.

Typ delegata to typ reprezentujący sygnaturę metody. Zmienna typu delegata może przechowywać dowolną zgodną metodę, taką jak wyrażenie lambda lub nazwana metoda, o ile jego typy parametrów i zwracany typ są zgodne.

Zadeklarujesz typ delegata za pomocą słowa kluczowego delegate :

delegate int Transform(int value);

Ta deklaracja mówi: "Transform jest typem delegata dla metod, które akceptują jeden int i zwracają int." Następnie można przypisać wyrażenie lambda lub nazwaną metodę do zmiennej tego typu.

Transform doubler = x => x * 2;    // assign a lambda expression
Transform squarer = Square;         // assign a named method

Console.WriteLine(doubler(5));      // 10
Console.WriteLine(squarer(5));      // 25

static int Square(int value) => value * value;

Zarówno, doubler jak i squarer przechowują wartość typu Transform. Wywołujesz je dokładnie tak, jak zwykłe metody. Kompilator sprawdza, czy przypisana wartość jest zgodna z zadeklarowaną sygnaturą.

Wbudowane typy delegatów: Func i Action

Deklarowanie niestandardowego typu delegata dla każdej sytuacji może być powtarzalne. .NET zapewnia dwie rodziny typów delegatów ogólnych, Func i Action, które obejmują większość scenariuszy, więc rzadko trzeba użyć delegate słowo kluczowe samodzielnie.

Obie rodziny występują w wersjach z od zera do szesnastu parametrów typu wejściowego, więc skalują się do dowolnej liczby danych wejściowych. Kluczową różnicą między dwiema rodzinami jest:

  • System.Func<T,TResult> (i Func<T1, T2, TResult>itd.) reprezentuje metodę zwracającą wartość. Ostatni parametr typu jest zawsze typem zwracanym, wszystkie wcześniejsze są parametrami wejściowymi.
  • System.Action<T> (i Action<T1, T2>itd.) reprezentuje metodę, która nie zwraca niczego (void). Wszystkie parametry typu są typami wejściowymi. System.Action bez parametrów typu reprezentuje metodę bez danych wejściowych i bez zwracanej wartości.

Na przykład Func<int, int, int> opisuje metodę z dwoma int danymi wejściowymi i jednym int wynikiem. Action<string> opisuje metodę z jednym string wejściem i bez zwracanej wartości.

Func<int, int, int> add = (left, right) => left + right;
Action<string> report = message => Console.WriteLine($"Report: {message}");

int total = add(5, 9);
report($"5 + 9 = {total}");

Użyj opisowych nazw parametrów w wyrażeniach lambda, aby czytelnicy mogli zrozumieć intencję bez skanowania pełnej treści metody.

Przekazywanie wyrażenia lambda do metody

Gdy metoda deklaruje parametr Func lub Action, wywołujące przekazują wyrażenie lambda zgodne z tym typem delegata. Kompilator sprawdza, czy typy parametrów lambda i typ zwracany są zgodne z zadeklarowanym typem delegata. Jeśli nie są one zgodne, kod nie zostanie skompilowany.

int[] numbers = [1, 2, 3, 4, 5, 6];
int[] evenNumbers = Filter(numbers, value => value % 2 == 0).ToArray();

Console.WriteLine(string.Join(", ", evenNumbers));

Metoda Filter deklaruje Func<int, bool> parametr o nazwie predicate. Typ Func<int, bool> informuje wywołującego o oczekiwanym kształcie: jedno int wejście, jeden bool wynik. Obiekt wywołujący przekazuje value => value % 2 == 0 jako argument. Ten wzorzec pojawia się w wielu interfejsach API LINQ i wielu interfejsach API .NET.

Zachowaj niezależne wyrażenia lambda

Wyrażenie lambda może odwoływać się do zmiennych z otaczającego kodu. Przechwytywanie oznacza, że lambda przechowuje odwołanie do zmiennej zadeklarowanej poza własną treścią. Kombinacja lambda i przechwyconych zmiennych jest nazywana zamknięciem.

Jeśli nie musisz nic przechwytywać, dodaj modyfikator static do wyrażenia lambda. Statyczna lambda może używać tylko własnych parametrów i wartości zadeklarowanych wewnątrz jej treści. Nie może przechwytywać zmiennych lokalnych ani stanu wystąpienia z otaczającego zakresu.

Func<int, bool> isEven = static value => value % 2 == 0;

Console.WriteLine(isEven(14));
Console.WriteLine(isEven(15));

Statyczne lambdy jasno wyjaśniają intencję i zapobiegają przypadkowemu przechwytywaniu.

Użyj parametrów odrzucania, gdy dane wejściowe są nieistotne

Czasami podpis delegata zawiera parametry, których nie potrzebujesz. Użyj odrzucenia _ , aby jawnie zasygnalizować ten wybór.

pl-PL: Typowe przykłady obejmują obsługę zdarzeń, gdzie nie używasz sender lub EventArgs, wywołania zwrotne, w których potrzebujesz tylko niektórych z kilku danych wejściowych, oraz przeciążenia LINQ, które oferują indeks, którego nie używasz.

Action<int, int, string> statusUpdate = (_, _, message) => Console.WriteLine(message);
statusUpdate(200, 42, "Operation completed");

Odrzuty poprawiają czytelność, gdyż pokazują, które parametry są istotne.

Zdarzenia zapewniają opcjonalne powiadomienia

Zdarzenie jest mechanizmem używanym przez jeden obiekt (wydawca) do powiadamiania innych obiektów (subskrybentów) w przypadku wystąpienia czegoś. Wydawca nie musi wiedzieć, kto nasłuchuje ani ilu subskrybentów istnieje. Subskrybenci wybierają opcję uczestnictwa.

Zdarzenia są tworzone na delegatach. Zdarzenie to pole typu delegat z dodatkowymi ograniczeniami wymuszonymi przez słowo kluczowe event: kod zewnętrzny może tylko subskrybować (+=) lub anulować subskrypcję (-=) zdarzenia; tylko klasa, która deklaruje zdarzenie, może je wywołać (podnieść).

Konwencja .NET dla typów delegatów zdarzeń to System.EventHandler<TEventArgs>, gdzie T jest typem danych zawartych w powiadomieniu. Jego podpis zawsze ma dwa parametry: sender (obiekt, który zgłosił zdarzenie) i dane zdarzenia typu T.

MessagePublisher publisher = new();
publisher.MessagePublished += (_, message) => Console.WriteLine($"Received: {message}");

publisher.Publish("Records updated");

Przechodzenie przez kod:

  • MessagePublisher event EventHandler<string>? MessagePublished deklaruje. Słowo event kluczowe oznacza, że osoby wywołujące mogą subskrybować lub anulować subskrypcji — nie mogą wywoływać ich bezpośrednio.
  • publisher.MessagePublished += (_, message) => ... subskrybuje korzystając z wyrażenia lambda. _ odrzuca parametr sender ponieważ ten handler go nie potrzebuje.
  • publisher.Publish("Records updated") zgłasza zdarzenie i uruchamia każdą subskrybowaną procedurę obsługującą.

Subskrybowanie jest opcjonalne. Metoda ?.Invoke(...) w metodzie Publish oznacza, że zdarzenie jest wywoływane tylko wtedy, gdy co najmniej jeden subskrybent jest dołączony. Wydawca zgłasza wydarzenie bez znajomości lub dbania o to, czy ktoś słucha.

Zobacz także