Lambda-expressies, delegaten en gebeurtenissen

Tip

Nieuw bij het ontwikkelen van software? Begin eerst met de handleiding Aan de slag. Bouw hier kerntypen en methodevaardigheden voordat u lambda-expressies gebruikt.

Soms wilt u een klein stukje gedrag, een functie, rechtstreeks doorgeven aan een andere methode. U kunt bijvoorbeeld een lijst filteren, maar de filtervoorwaarde verandert afhankelijk van de situatie. In plaats van een afzonderlijke benoemde methode te schrijven voor elke mogelijke voorwaarde, geeft u de voorwaarde zelf door als argument.

Lambda-expressies zijn de C#-functie die dit mogelijk maakt. Een lambda-expressie is een compacte, inline-functie die u schrijft zonder deze een naam te geven. U gebruikt de pijloperator => om de lijst met parameters te scheiden van de hoofdtekst:

x => x * 2

Lezen van links naar rechts: x is de invoerparameter, => betekent 'gaat naar', en x * 2 is de hoofdtekst. Hiermee wordt de geretourneerde waarde berekend. Als er geen parameters of meer dan één parameters zijn, verpakt u deze tussen haakjes: () => 42 of (left, right) => left + right.

Delegaten ondersteunen lambda-expressies

Als u een lambda-expressie wilt gebruiken, moet de C#-compiler twee dingen weten: de typen parameters en het retourtype. Deze beschrijving, de parametertypen plus het retourtype, wordt een gemachtigdentype genoemd.

Een gemachtigdentype is een type dat een methodehandtekening vertegenwoordigt. Een variabele van een gemachtigdentype kan elke overeenkomende methode bevatten, zoals een lambda-expressie of een benoemde methode, zolang de parametertypen en het resultaattype overeenkomen.

U declareert een type gemachtigde met het delegate trefwoord:

delegate int Transform(int value);

Deze declaratie zegt: "Transform is een gemachtigde type voor methoden die een int accepteren en een intretourneren." Vervolgens kunt u een lambda-expressie of een benoemde methode toewijzen aan een variabele van dat type:

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;

Beide doubler en squarer bevatten een waarde van het type Transform. U noemt ze precies zoals reguliere methoden. De compiler controleert of alles wat u toewijst overeenkomt met de gedeclareerde handtekening.

Ingebouwde delegatietypen: Func en Action

Het declareren van een aangepast type gedelegeerde voor elke situatie kan herhalend zijn. .NET biedt twee groepen algemene gemachtigdentypen, Func en Action, die de meeste scenario's omvatten, dus u hoeft het trefwoord delegate zelden zelf te gebruiken.

Beide families worden geleverd in versies met nul tot zestien invoertypeparameters, zodat ze worden geschaald naar een willekeurig aantal invoer. Het belangrijkste verschil tussen de twee families is:

  • System.Func<T,TResult> (en Func<T1, T2, TResult>, enzovoort) vertegenwoordigt een methode die een waarde retourneert. De laatste typeparameter is altijd het retourtype; alle eerdere typen zijn invoertypen.
  • System.Action<T> (en Action<T1, T2>, enzovoort) vertegenwoordigt een methode die niets retourneert (void). Alle typeparameters zijn invoertypen. System.Action zonder typeparameters vertegenwoordigt een methode zonder invoer en geen retourwaarde.

Beschrijft bijvoorbeeld Func<int, int, int> een methode met twee int invoerwaarden en één int resultaat. Action<string> beschrijft een methode met één string invoer en geen retourwaarde.

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}");

Gebruik beschrijvende parameternamen in lambdas, zodat lezers de intentie kunnen begrijpen zonder de volledige hoofdtekst van de methode te scannen.

Een lambda-expressie doorgeven aan een methode

Wanneer een methode een Func of Action parameter declareert, geven aanroepers een lambda-expressie door die overeenkomt met dat delegate-type. De compiler controleert of de parametertypen van de lambda en het retourtype overeenkomen met het gedeclareerde gemachtigdetype. Als ze niet overeenkomen, wordt de code niet gecompileerd.

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

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

De Filter methode declareert een parameter met de Func<int, bool> naam predicate. Het Func<int, bool> type vertelt bellers de verwachte shape: één int invoer, één bool resultaat. De aanroeper geeft dit argument door value => value % 2 == 0 . Dit patroon wordt weergegeven in LINQ en veel .NET API's.

Lambda-expressies zelfstandig houden

Een lambda-expressie kan verwijzen naar variabelen uit de omringende code. Vastleggen betekent dat de lambda een referentie vasthoudt naar een variabele die buiten zijn codeblok is gedeclareerd. De combinatie van de lambda en de variabelen die worden vastgelegd, wordt een sluiting genoemd.

Wanneer u niets hoeft vast te leggen, voegt u de static wijzigingsfunctie toe aan de lambda. Een statische lambda kan alleen eigen parameters en waarden gebruiken die in de hoofdtekst zijn gedeclareerd. Er kunnen geen lokale variabelen of instantiestaat worden vastgelegd vanuit de insluitende scope.

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

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

Statische lambdas maken intenties duidelijk en voorkomen onbedoelde opnamen.

Gebruik parameters om te negeren wanneer invoer niet relevant is

Soms bevat een handtekening voor gemachtigden parameters die u niet nodig hebt. Gebruik de verwijdering _ om die keuze expliciet aan te geven.

Veelvoorkomende voorbeelden zijn gebeurtenishandlers waarbij u geen gebruik maakt van sender, callbacks waarbij u slechts enkele van de verschillende invoergegevens nodig hebt, en LINQ-overbelastingen die een index bieden die u niet gebruikt.

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

Verworpen elementen verbeteren de leesbaarheid, omdat ze aangeven welke parameters belangrijk zijn.

Gebeurtenissen bieden optionele meldingen

Een gebeurtenis is een mechanisme dat door het ene object (de uitgever) wordt gebruikt om andere objecten (de abonnees) op de hoogte te stellen wanneer er iets gebeurt. De uitgever hoeft niet te weten wie luistert of hoeveel abonnees er zijn. Abonnees kiezen ervoor om zich aan te kiezen.

Gebeurtenissen zijn gebaseerd op delegates. Een gebeurtenis is een gemachtigdenveld met extra beperkingen die worden afgedwongen door het event trefwoord: externe code kan zich alleen abonneren (+=) of afmelden (-=) van de gebeurtenis. Alleen de klasse die de gebeurtenis declareert, kan deze aanroepen (genereren).

De .NET-conventie voor typen gebeurtenisdelegen is System.EventHandler<TEventArgs>, waarbij T het type gegevens is dat is opgenomen in de melding. De handtekening heeft altijd twee parameters: het sender (object dat de gebeurtenis heeft gegenereerd) en de gebeurtenisgegevens van het type T.

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

publisher.Publish("Records updated");

De code doornemen:

  • MessagePublisher verklaart event EventHandler<string>? MessagePublished. Het event trefwoord betekent dat bellers zich alleen kunnen abonneren of afmelden. Ze kunnen het niet rechtstreeks aanroepen.
  • publisher.MessagePublished += (_, message) => ... gebruikt een lambda-expressie om zich te abonneren. De _ gooit de sender parameter weg omdat deze handler deze niet nodig heeft.
  • publisher.Publish("Records updated") genereert de gebeurtenis en voert elke geabonneerde handler uit.

Abonneren is optioneel. Het ?.Invoke(...) element in de Publish methode betekent dat de gebeurtenis alleen wordt geactiveerd wanneer er ten minste één abonnee is gekoppeld. De uitgever roept de gebeurtenis op zonder te weten of iemand luistert.

Zie ook