Lambda-kifejezések és névtelen függvények

A lambda kifejezéssel névtelen függvényt hozhat létre. A lambda deklarációs operátorral => elválaszthatja a lambda paraméterlistáját a törzsétől. A lambda kifejezés a következő két űrlap bármelyike lehet:

  • A lambda kifejezés, amelynek törzse kifejezéssel rendelkezik:

    (input-parameters) => expression
    
  • Utasítás lambda , amelynek törzse egy utasításblokk:

    (input-parameters) => { <sequence-of-statements> }
    

Lambda-kifejezés létrehozásához a lambda operátor bal oldalán adja meg a bemeneti paramétereket (ha vannak ilyenek), a másik oldalon pedig egy kifejezést vagy utasításblokkot.

Bármely lambda-kifejezés delegált típussá alakítható. A lambda kifejezés konvertálható delegálási típusát a paraméterek típusai és a visszatérési érték határozza meg. Ha egy lambda kifejezés nem ad vissza értéket, akkor az egyik delegált típussá alakítható Action át, ellenkező esetben a delegálttípusok egyikére Func konvertálható. Például egy lambda kifejezés, amely két paraméterrel rendelkezik, és nem ad vissza értéket, delegálttá Action<T1,T2> alakítható. Egy lambda kifejezés, amely egy paraméterrel rendelkezik, és egy értéket ad vissza, delegálttá Func<T,TResult> alakítható. Az alábbi példában a lambda kifejezés x => x * x, amely egy elnevezett x paramétert ad vissza, és a négyzetes értéket x adja vissza, egy delegált típusú változóhoz van rendelve:

Func<int, int> square = x => x * x;
Console.WriteLine(square(5));
// Output:
// 25

A kifejezési lambdas kifejezésfatípusokká is konvertálható, ahogy az alábbi példa is mutatja:

System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
Console.WriteLine(e);
// Output:
// x => (x * x)

A lambda kifejezéseket bármely olyan kódban használhatja, amely delegált típusok vagy kifejezésfák példányait igényli, például argumentumként a Task.Run(Action) metódushoz, hogy átadja a háttérben végrehajtandó kódot. Lambda-kifejezéseket is használhat a LINQ C#-ban való írásakor, ahogy az alábbi példa is mutatja:

int[] numbers = { 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(x => x * x);
Console.WriteLine(string.Join(" ", squaredNumbers));
// Output:
// 4 9 16 25

Ha metódusalapú szintaxissal hívja meg a Enumerable.Select metódust az System.Linq.Enumerable osztályban, például a LINQ-ban az Objektumokhoz és a LINQ-ból XML-be, a paraméter egy delegált típus System.Func<T,TResult>. Amikor meghívja a Queryable.Select metódust az osztályban, például a System.Linq.Queryable LINQ-ból SQL-be, a paramétertípus egy kifejezésfatípus Expression<Func<TSource,TResult>>. Mindkét esetben ugyanazt a lambda kifejezést használhatja a paraméter értékének megadásához. Ez hasonlóvá teszi a két Select hívást, bár valójában a lambdákból létrehozott objektumok típusa eltérő.

Lambdas kifejezés

Az operátor jobb oldalán => lévő lambda kifejezést lambda kifejezésnek nevezzük. A lambda kifejezés a kifejezés eredményét adja vissza, és a következő alapformát veszi fel:

(input-parameters) => expression

A lambda kifejezés törzse egy metódushívásból állhat. Ha azonban a .NET Common Language Runtime (CLR) környezetén kívül kiértékelt kifejezésfákat hoz létre, például az SQL Serverben, akkor nem szabad metódushívásokat használnia a lambda kifejezésekben. A metódusoknak nincs jelentésük a .NET Common Language Runtime (CLR) környezetén kívül.

Utasítás lambdas

A lambda utasítás hasonlít a lambda kifejezésre, azzal a kivételével, hogy az utasítások zárójelek közé vannak kapcsos zárójelek közé zárva:

(input-parameters) => { <sequence-of-statements> }

A lambda utasítás törzse tetszőleges számú utasításból állhat; a gyakorlatban azonban általában legfeljebb kettő vagy három van.

Action<string> greet = name =>
{
    string greeting = $"Hello {name}!";
    Console.WriteLine(greeting);
};
greet("World");
// Output:
// Hello World!

Az utasítási lambdák nem használhatók kifejezésfák létrehozására.

Lambda-kifejezés bemeneti paraméterei

A lambda kifejezés bemeneti paramétereit zárójelek között kell megadnia. Nulla bemeneti paraméter megadása üres zárójelekkel:

Action line = () => Console.WriteLine();

Ha egy lambda kifejezés csak egy bemeneti paraméterrel rendelkezik, a zárójelek megadása nem kötelező:

Func<double, double> cube = x => x * x * x;

Két vagy több bemeneti paraméter vesszővel van elválasztva:

Func<int, int, bool> testForEquality = (x, y) => x == y;

Előfordulhat, hogy a fordító nem tudja következtetni a bemeneti paraméterek típusaira. A típusokat explicit módon is megadhatja az alábbi példában látható módon:

Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;

A bemeneti paramétertípusoknak explicitnek vagy minden implicitnek kell lenniük; ellenkező esetben CS0748-fordítóhiba lép fel.

Elvetésekkel megadhat két vagy több olyan bemeneti paramétert egy lambda kifejezéshez, amelyek nem szerepelnek a kifejezésben:

Func<int, int, int> constant = (_, _) => 42;

A Lambda elvetésének paraméterei akkor lehetnek hasznosak, ha lambdakifejezéssel biztosít egy eseménykezelőt.

Feljegyzés

A visszamenőleges kompatibilitás érdekében, ha csak egyetlen bemeneti paraméter neve van elnevezve _, akkor a lambda kifejezésen _ belül a rendszer ennek a paraméternek a neveként kezeli.

A C# 12-től kezdődően megadhat alapértelmezett értékeket a lambda kifejezések paramétereihez. Az alapértelmezett paraméterértékek szintaxisa és korlátozásai megegyeznek a metódusokkal és a helyi függvényekkel. Az alábbi példa egy lambda kifejezést deklarál egy alapértelmezett paraméterrel, majd egyszer az alapértelmezett, egyszer pedig két explicit paraméterrel hívja meg:

var IncrementBy = (int source, int increment = 1) => source + increment;

Console.WriteLine(IncrementBy(5)); // 6
Console.WriteLine(IncrementBy(5, 2)); // 7

A tömbökkel rendelkező params lambdakifejezéseket paraméterként is deklarálhatja:

var sum = (params int[] values) =>
{
    int sum = 0;
    foreach (var value in values) 
        sum += value;
    
    return sum;
};

var empty = sum();
Console.WriteLine(empty); // 0

var sequence = new[] { 1, 2, 3, 4, 5 };
var total = sum(sequence);
Console.WriteLine(total); // 15

A frissítések részeként, amikor egy alapértelmezett paraméterrel rendelkező metóduscsoport egy lambda kifejezéshez van rendelve, a lambda kifejezés is ugyanazzal az alapértelmezett paraméterrel rendelkezik. A tömbparaméterrel params rendelkező metóduscsoportok lambdakifejezéshez is hozzárendelhetők.

A paraméterekként alapértelmezett paraméterekkel vagy params tömbökkel rendelkező Lambda-kifejezések nem rendelkeznek olyan természetes típusokkal, amelyek megfelelnek Func<> vagy Action<> típusok. Megadhatja azonban az alapértelmezett paraméterértékeket tartalmazó delegálttípusokat:

delegate int IncrementByDelegate(int source, int increment = 1);
delegate int SumDelegate(params int[] values);

Deklarációkkal var rendelkező implicit típusú változókat is használhat a deklaráció típusának meghatározásához. A fordító a megfelelő delegálási típust szintetizálja.

További információkért tekintse meg a lambdakifejezések alapértelmezett paramétereinek funkciós specifikációját.

Async lambdas

Az aszinkron feldolgozást tartalmazó lambdakifejezéseket és utasításokat egyszerűen létrehozhatja az aszinkron módon, és várhatja a kulcsszavakat. Az alábbi Windows Forms-példa például egy eseménykezelőt tartalmaz, amely meghívja és várja az aszinkron metódust. ExampleMethodAsync

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        button1.Click += button1_Click;
    }

    private async void button1_Click(object sender, EventArgs e)
    {
        await ExampleMethodAsync();
        textBox1.Text += "\r\nControl returned to Click event handler.\n";
    }

    private async Task ExampleMethodAsync()
    {
        // The following line simulates a task-returning asynchronous process.
        await Task.Delay(1000);
    }
}

Ugyanezt az eseménykezelőt aszinkron lambda használatával is hozzáadhatja. A kezelő hozzáadásához adjon hozzá egy async módosítót a lambda paraméterlista előtt, ahogy az alábbi példa is mutatja:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        button1.Click += async (sender, e) =>
        {
            await ExampleMethodAsync();
            textBox1.Text += "\r\nControl returned to Click event handler.\n";
        };
    }

    private async Task ExampleMethodAsync()
    {
        // The following line simulates a task-returning asynchronous process.
        await Task.Delay(1000);
    }
}

Az aszinkron metódusok létrehozásáról és használatáról további információt az Aszinkron programozás aszinkronnal és várakozással című témakörben talál.

Lambda-kifejezések és -csuplok

A C#-nyelv beépített támogatást nyújt a kukákhoz. A lambda kifejezés argumentumaként megadhatja a rekordot, a lambda kifejezés pedig egy rekordot is visszaadhat. Bizonyos esetekben a C#-fordító típusmegkövetkeztetést használ a rekordösszetevők típusának meghatározásához.

A rekordokat úgy határozhatja meg, hogy zárójelekbe rendezi az összetevők vesszővel tagolt listáját. Az alábbi példa három összetevőből álló rekordot használ egy számsorozat egy lambda kifejezésnek való átadásához, amely minden értéket megdupláz, és egy rekordot ad vissza három összetevővel, amelyek a szorzások eredményét tartalmazzák.

Func<(int, int, int), (int, int, int)> doubleThem = ns => (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
// Output:
// The set (2, 3, 4) doubled: (4, 6, 8)

Általában a tuple mezőinek neve Item1, Item2és így tovább. A nevesített összetevőket tartalmazó rekordokat azonban definiálhatja, ahogyan az alábbi példa is szemlélteti.

Func<(int n1, int n2, int n3), (int, int, int)> doubleThem = ns => (2 * ns.n1, 2 * ns.n2, 2 * ns.n3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");

A C#-rekordokról további információt a Tuple-típusok című témakörben talál.

Lambdas standard lekérdezési operátorokkal

A LINQ to Objects más implementációk mellett rendelkezik egy bemeneti paraméterlel, amelynek típusa az Func<TResult> általános meghatalmazottak családjának egyike. Ezek a meghatalmazottak típusparaméterekkel határozzák meg a bemeneti paraméterek számát és típusát, valamint a meghatalmazott visszatérési típusát. Func A delegáltak olyan felhasználó által definiált kifejezések beágyazásához hasznosak, amelyeket a forrásadatok egy halmazának minden elemére alkalmazunk. Vegyük például a Func<T,TResult> delegált típusát:

public delegate TResult Func<in T, out TResult>(T arg)

A meghatalmazott példányként példányosítható olyan példányként Func<int, bool> , amely int bemeneti paraméter, és bool a visszatérési érték. A visszatérési érték mindig az utolsó típusparaméterben van megadva. Definiál például Func<int, string, bool> egy delegáltat két bemeneti paraméterrel és intstringegy visszatérési típussal bool. A következő Func delegált a meghíváskor logikai értéket ad vissza, amely azt jelzi, hogy a bemeneti paraméter ötnek felel-e meg:

Func<int, bool> equalsFive = x => x == 5;
bool result = equalsFive(4);
Console.WriteLine(result);   // False

Lambda-kifejezést akkor is megadhat, ha az argumentumtípus egy Expression<TDelegate>, például a típusban definiált szabványos lekérdezési operátorokban Queryable . Argumentum megadásakor Expression<TDelegate> a lambda kifejezésfára lesz lefordítva.

Az alábbi példa a szabványos lekérdezési operátort Count használja:

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
int oddNumbers = numbers.Count(n => n % 2 == 1);
Console.WriteLine($"There are {oddNumbers} odd numbers in {string.Join(" ", numbers)}");

A fordító kikövetkezheti a bemeneti paraméter típusát, vagy explicit módon is megadhatja. Ez a lambda-kifejezés megszámolja azokat az egész számokat (n), amelyek két részre osztva 1-ből áll.

Az alábbi példa egy olyan sorozatot hoz létre, amely a numbers tömb 9 előtti összes elemét tartalmazza, mivel ez az első szám a sorozatban, amely nem felel meg a feltételnek:

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstNumbersLessThanSix = numbers.TakeWhile(n => n < 6);
Console.WriteLine(string.Join(" ", firstNumbersLessThanSix));
// Output:
// 5 4 1 3

Az alábbi példa több bemeneti paramétert határoz meg zárójelekbe foglalva. A metódus a tömb összes elemét numbers visszaadja, amíg olyan számot nem talál, amelynek értéke kisebb, mint a tömb sorszáma:

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstSmallNumbers = numbers.TakeWhile((n, index) => n >= index);
Console.WriteLine(string.Join(" ", firstSmallNumbers));
// Output:
// 5 4

A lambda kifejezéseket nem használhatja közvetlenül a lekérdezési kifejezésekben, de metódushívásokban is használhatja őket a lekérdezési kifejezésekben, ahogyan az alábbi példa is mutatja:

var numberSets = new List<int[]>
{
    new[] { 1, 2, 3, 4, 5 },
    new[] { 0, 0, 0 },
    new[] { 9, 8 },
    new[] { 1, 0, 1, 0, 1, 0, 1, 0 }
};

var setsWithManyPositives = 
    from numberSet in numberSets
    where numberSet.Count(n => n > 0) > 3
    select numberSet;

foreach (var numberSet in setsWithManyPositives)
{
    Console.WriteLine(string.Join(" ", numberSet));
}
// Output:
// 1 2 3 4 5
// 1 0 1 0 1 0 1 0

Következtetés beírása Lambda-kifejezésekben

Lambdas írásakor gyakran nem kell típust megadnia a bemeneti paraméterekhez, mert a fordító a lambda törzse, a paramétertípusok és egyéb tényezők alapján tud következtetni a típusra a C# nyelv specifikációjában leírtak szerint. A szabványos lekérdezési operátorok többsége esetében az első bemenet a forrásütemezés elemeinek típusa. Ha lekérdezést IEnumerable<Customer>hajt végre, akkor a bemeneti változó egy objektumnak Customer lesz kikövetkeztetve, ami azt jelenti, hogy hozzáféréssel rendelkezik a metódusaihoz és tulajdonságaihoz:

customers.Where(c => c.City == "London");

A lambdas típuskövetkeztetésének általános szabályai a következők:

  • A lambdának ugyanannyi paramétert kell tartalmaznia, mint a delegált típusa.
  • A lambda minden bemeneti paraméterének implicit módon konvertálhatónak kell lennie a megfelelő delegált paraméterre.
  • A lambda visszatérési értékének (ha van ilyen) implicit módon konvertálhatónak kell lennie a meghatalmazott visszatérési típusára.

Lambda-kifejezés természetes típusa

A lambda kifejezés önmagában nem rendelkezik típussal, mert a közös típusrendszernek nincs belső fogalma a "lambda kifejezésről". Néha azonban kényelmes informálisan beszélni a lambda kifejezés "típusáról". Ez a nem hivatalos "típus" arra a delegált típusra vagy Expression típusra utal, amelyre a lambda kifejezés konvertálva van.

A C# 10-től kezdődően a lambda kifejezésnek természetes típusa lehet. Ahelyett, hogy deklarálásra kényszeríti a deklarált típust, például Func<...> egy lambda kifejezést, Action<...> a fordító a lambda kifejezésből következtethet a delegálás típusára. Vegyük például a következő deklarációt:

var parse = (string s) => int.Parse(s);

A fordító arra következtethet parse , hogy egy Func<string, int>. A fordító kiválaszt egy elérhető Func vagy Action meghatalmazottat, ha létezik megfelelő. Ellenkező esetben egy delegálttípust szintetizál. A delegált típusa például akkor szintetizálódik, ha a lambda kifejezés paraméterekkel rendelkezik ref . Ha a lambda kifejezés természetes típusú, akkor kevésbé explicit típushoz rendelhető, például System.ObjectSystem.Delegate:

object parse = (string s) => int.Parse(s);   // Func<string, int>
Delegate parse = (string s) => int.Parse(s); // Func<string, int>

A pontosan egy túlterheléssel rendelkező metóduscsoportoknak (vagyis a paraméterlisták nélküli metódusneveknek) természetes típusuk van:

var read = Console.Read; // Just one overload; Func<int> inferred
var write = Console.Write; // ERROR: Multiple overloads, can't choose

Ha lambda kifejezést System.Linq.Expressions.LambdaExpressionrendel hozzá , vagy System.Linq.Expressions.Expression, és a lambda természetes delegálási System.Linq.Expressions.Expression<TDelegate>típussal rendelkezik, a kifejezés természetes típusú, és a típusparaméter argumentumaként a természetes delegált típust használja:

LambdaExpression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Expression parseExpr = (string s) => int.Parse(s);       // Expression<Func<string, int>>

Nem minden lambda-kifejezés rendelkezik természetes típussal. Vegye figyelembe a következő deklarációt:

var parse = s => int.Parse(s); // ERROR: Not enough type info in the lambda

A fordító nem tud paramétertípust következtetni a következőre s: . Ha a fordító nem tud természetes típust kikövetkeztetni, deklarálnia kell a típust:

Func<string, int> parse = s => int.Parse(s);

Explicit visszatérési típus

A lambda kifejezés visszatérési típusa általában nyilvánvaló és kikövetkeztetett. Néhány nem működő kifejezés esetén:

var choose = (bool b) => b ? 1 : "two"; // ERROR: Can't infer return type

A C# 10-től kezdődően megadhatja a lambda kifejezés visszatérési típusát a bemeneti paraméterek előtt. Explicit visszatérési típus megadásakor zárójelben kell megadnia a bemeneti paramétereket:

var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>

Attribútumok

A C# 10-től kezdve attribútumokat adhat hozzá egy lambda kifejezéshez és annak paramétereihez. Az alábbi példa bemutatja, hogyan adhat hozzá attribútumokat egy lambda kifejezéshez:

Func<string?, int?> parse = [ProvidesNullCheck] (s) => (s is not null) ? int.Parse(s) : null;

Attribútumokat is hozzáadhat a bemeneti paraméterekhez, vagy visszaadhatja az értéket, ahogy az az alábbi példában is látható:

var concat = ([DisallowNull] string a, [DisallowNull] string b) => a + b;
var inc = [return: NotNullIfNotNull(nameof(s))] (int? s) => s.HasValue ? s++ : null;

Ahogy az előző példák is mutatják, zárójelbe kell helyeznie a bemeneti paramétereket, amikor attribútumokat ad hozzá egy lambda kifejezéshez vagy annak paramétereihez.

Fontos

A Lambda-kifejezések meghívása a mögöttes delegált típuson keresztül történik. Ez más, mint a metódusok és a helyi függvények. A meghatalmazott metódusa Invoke nem ellenőrzi az attribútumokat a lambda kifejezésen. Az attribútumoknak nincs hatása a lambda kifejezés meghívásakor. A lambda-kifejezések attribútumai hasznosak a kódelemzéshez, és tükröződés útján is felderíthetők. Ennek a döntésnek az egyik következménye, hogy a System.Diagnostics.ConditionalAttribute lambda kifejezésre nem alkalmazható.

Külső változók és változók hatókörének rögzítése Lambda-kifejezésekben

A Lambdas külső változókra hivatkozhat. Ezek a külső változók azok a változók , amelyek a lambda kifejezést meghatározó metódus hatókörében, vagy a lambda kifejezést tartalmazó típus hatókörében találhatók. Az ilyen módon rögzített változók akkor is a lambda kifejezésben való használatra vannak tárolva, ha a változók egyébként kimennek a hatókörből, és szemétként lesznek összegyűjtve. A külső változót mindenképpen hozzá kell rendelni ahhoz, hogy a lambda kifejezésben felhasználható legyen. Az alábbi példa a következő szabályokat mutatja be:

public static class VariableScopeWithLambdas
{
    public class VariableCaptureGame
    {
        internal Action<int>? updateCapturedLocalVariable;
        internal Func<int, bool>? isEqualToCapturedLocalVariable;

        public void Run(int input)
        {
            int j = 0;

            updateCapturedLocalVariable = x =>
            {
                j = x;
                bool result = j > input;
                Console.WriteLine($"{j} is greater than {input}: {result}");
            };

            isEqualToCapturedLocalVariable = x => x == j;

            Console.WriteLine($"Local variable before lambda invocation: {j}");
            updateCapturedLocalVariable(10);
            Console.WriteLine($"Local variable after lambda invocation: {j}");
        }
    }

    public static void Main()
    {
        var game = new VariableCaptureGame();

        int gameInput = 5;
        game.Run(gameInput);

        int jTry = 10;
        bool result = game.isEqualToCapturedLocalVariable!(jTry);
        Console.WriteLine($"Captured local variable is equal to {jTry}: {result}");

        int anotherJ = 3;
        game.updateCapturedLocalVariable!(anotherJ);

        bool equalToAnother = game.isEqualToCapturedLocalVariable(anotherJ);
        Console.WriteLine($"Another lambda observes a new value of captured variable: {equalToAnother}");
    }
    // Output:
    // Local variable before lambda invocation: 0
    // 10 is greater than 5: True
    // Local variable after lambda invocation: 10
    // Captured local variable is equal to 10: True
    // 3 is greater than 5: False
    // Another lambda observes a new value of captured variable: True
}

A lambda-kifejezések változó hatókörére a következő szabályok vonatkoznak:

  • A rögzített változók nem lesznek szemétgyűjtésben, amíg az arra hivatkozó meghatalmazott nem lesz jogosult a szemétgyűjtésre.
  • A lambda kifejezésben bevezetett változók nem láthatók a beágyazási módszerben.
  • A lambda kifejezés nem tud közvetlenül rögzíteni egy in, ref vagy out paramétert a beágyazási módszerből.
  • A lambda kifejezés visszatérési utasítása nem eredményezi a beágyazási metódus visszatérését.
  • A lambda kifejezések nem tartalmazhatnak goto, break vagy continue utasítást, ha a jump utasítás célja kívül esik a lambda kifejezésblokkon. Az is hiba, hogy a lambda kifejezésblokkon kívül van egy jump utasítás, ha a cél a blokkon belül van.

A módosító egy static lambda kifejezésre is alkalmazható, hogy megakadályozza a helyi változók vagy példányállapotok lambda általi véletlen rögzítését:

Func<double, double> square = static x => x * x;

A statikus lambda nem tudja rögzíteni a helyi változókat vagy a példány állapotát a hatókörökből, de hivatkozhat statikus tagokra és állandó definíciókra.

C# nyelvspecifikáció

További információ: A C# nyelv specifikációjának Névtelen függvénykifejezések szakasza.

További információ ezekről a funkciókról:

Lásd még