Lambda-uttryck, delegater och händelser

Tip

Är du nybörjare på att utveckla programvara? Börja med självstudierna Komma igång först. Skapa grundläggande typ- och metodkunskaper där innan du använder lambda-uttryck.

Ibland vill du skicka ett litet beteende, en funktion, direkt till en annan metod. Du kanske till exempel vill filtrera en lista, men filtreringsvillkoret ändras beroende på situationen. I stället för att skriva en separat namngiven metod för varje möjligt villkor skickar du själva villkoret som argument.

Lambda-uttryck är C#-funktionen som gör det möjligt. Ett lambda-uttryck är en kompakt, infogad funktion som du skriver utan att ge det ett namn. Du använder piloperatorn => för att separera parameterlistan från brödtexten:

x => x * 2

Läsning från vänster till höger: x är indataparametern, => betyder "går till" och x * 2 är brödtexten. Den beräknar det returnerade värdet. Om det inte finns några parametrar eller fler än en, omsluter du dem i parenteser: () => 42 eller (left, right) => left + right.

Ombud stöder lambda-uttryck

Om du vill använda ett lambda-uttryck behöver C#-kompilatorn veta två saker: parametrarnas typer och returtypen. Den beskrivningen, parametertyperna plus returtyp, kallas för en ombudstyp.

En ombudstyp är en typ som representerar en metodsignatur. En variabel av en ombudstyp kan innehålla valfri matchande metod, till exempel ett lambda-uttryck eller en namngiven metod, så länge som dess parametertyper och returtyp matchar.

Du deklarerar en ombudstyp med nyckelordet delegate :

delegate int Transform(int value);

I den här deklarationen står det: "Transform är en delegattyp för metoder som accepterar en int och returnerar en int." Du kan sedan tilldela ett lambda-uttryck eller en namngiven metod till en variabel av den typen:

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;

Både doubler och squarer innehåller ett värde av typen Transform. Du kallar dem exakt som vanliga metoder. Kompilatorn verifierar att det du tilldelar matchar den deklarerade signaturen.

Inbyggda ombudstyper: Func och Action

Att deklarera en anpassad delegattyp för varje situation kan vara repetitivt. .NET innehåller två familjer med allmänna ombudstyper, Func och Action, som täcker de flesta scenarier, så du behöver sällan använda nyckelordet delegate själv.

Båda familjerna finns i versioner med parametrarna noll till sexton indatatyper, så de skalas till valfritt antal indata. Den viktigaste skillnaden mellan de två familjerna är:

  • System.Func<T,TResult> (och Func<T1, T2, TResult>, och så vidare) representerar en metod som returnerar ett värde. Den sista typparametern är alltid returtypen. alla tidigare är indatatyper.
  • System.Action<T> (och Action<T1, T2>, och så vidare) representerar en metod som inte returnerar något (void). Alla typparametrar är indatatyper. System.Action utan typparametrar representerar en metod utan indata och inget returvärde.

Beskriver till exempel Func<int, int, int> en metod med två int indata och ett int resultat. Action<string> beskriver en metod med en string indata och inget returvärde.

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

Använd beskrivande parameternamn i lambdas så att läsarna kan förstå avsikten utan att genomsöka hela metodtexten.

Skicka ett lambda-uttryck till en metod

När en metod deklarerar en Func eller Action-parameter skickar anropare ett lambda-uttryck som matchar delegattypen. Kompilatorn kontrollerar att lambda-parametertyperna och returtypen matchar den deklarerade delegattypen. Om de inte matchar kompileras inte koden.

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

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

Metoden Filter deklarerar en Func<int, bool> parameter med namnet predicate. Typen Func<int, bool> talar om för anropare den förväntade formen: en int inmatning, ett bool resultat. Anroparen skickar value => value % 2 == 0 som argumentet. Det här mönstret visas i hela LINQ och många .NET API:er.

Behåll lambda-uttryck fristående

Ett lambda-uttryck kan referera till variabler från den omgivande koden. Avbildning innebär att lambda innehåller en referens till en variabel som deklarerats utanför sin egen kropp. Kombinationen av lambda och de variabler som den fångar kallas för en stängning.

När du inte behöver avbilda något lägger du till static modifieraren i lambda. En statisk lambda kan bara använda sina egna parametrar och värden som deklareras i dess brödtext. Den kan inte fånga lokala variabler eller instanstillstånd från det omslutande omfånget.

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

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

Statiska lambdas gör avsikten tydlig och förhindrar oavsiktliga avbildningar.

Använda parametrar för att ignorera när indata är irrelevanta

Ibland innehåller en ombudssignatur parametrar som du inte behöver. Använd ignorera _ för att uttryckligen signalera det valet.

Vanliga exempel är händelsehanterare där du inte använder sender eller EventArgs, återanrop där du bara behöver några av flera indata och LINQ-överlagringar som ger ett index som du inte använder.

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

Uteslutningar förbättrar läsbarheten eftersom de visar vilka parametrar som är viktiga.

Händelser ger valfria meddelanden

En händelse är en mekanism som ett objekt ( utgivaren) använder för att meddela andra objekt ( prenumeranterna) när något händer. Utgivaren behöver inte veta vem som lyssnar eller hur många prenumeranter det finns. Prenumeranter väljer att anmäla sig.

Händelser byggs på delegater. En händelse är ett ombudsfält med extra begränsningar som tillämpas av nyckelordet event : extern kod kan bara prenumerera (+=) eller avbryta prenumerationen (-=) från händelsen. Endast klassen som deklarerar händelsen kan anropa (höja) den.

Den .NET konventionen för händelsedelegattyper är System.EventHandler<TEventArgs>, där T är den typ av data som ingår i meddelandet. Signaturen har alltid två parametrar: sender (objektet som skapade händelsen) och händelsedata av typen T.

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

publisher.Publish("Records updated");

Gå igenom koden:

  • MessagePublisher deklarerar event EventHandler<string>? MessagePublished. Nyckelordet event innebär att anropare bara kan prenumerera eller avbryta prenumerationen – de kan inte anropa det direkt.
  • publisher.MessagePublished += (_, message) => ... prenumererar med ett lambda-uttryck. Tar _ bort parametern sender eftersom den här hanteraren inte behöver den.
  • publisher.Publish("Records updated") genererar händelsen och kör varje prenumerationshanterare.

Det är valfritt att prenumerera. ?.Invoke(...) i Publish-metoden innebär att händelsen endast utlöses när minst en prenumerant är ansluten. Utgivaren utlöser händelsen utan att veta eller bry sig om någon lyssnar.

Se även