Megosztás a következőn keresztül:


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

Névtelen függvény létrehozásához egy lambdakifejezést kell használnia. 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 alak bármelyike lehet:

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

    (input-parameters) => expression
    
  • lambda utasítás, 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 átalakítható delegált típussá. A paraméterek típusai és a visszatérési érték határozza meg azt a delegálási típust, amelyre a lambda kifejezés konvertálható. Ha egy lambda kifejezés nem ad vissza értéket, akkor az Action delegált típus egyikére konvertálható; ellenkező esetben az egyik Func delegált típussá alakítható. Például egy lambda kifejezés, amely két paraméterrel rendelkezik, és nem ad vissza értéket, átalakítható Action<T1,T2> delegálttá. A lambda kifejezés, amely egy paraméterrel rendelkezik, és egy értéket ad vissza, átalakítható Func<T,TResult> delegálttá. Az alábbi példában a lambda kifejezés x => x * x, amely egy x nevű paramétert határoz meg, és a négyzetes x értékét adja vissza, 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 lambda kifejezések kifejezésfa típussá is konvertálhatók, 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)

Lambda-kifejezéseket minden olyan kódban használhat, amely delegált típusú vagy kifejezésfákat igénylő példányokat igényel. Ilyen például a Task.Run(Action) metódus argumentuma, amely a háttérben végrehajtandó kódot adja át. Lambda-kifejezéseket is használhat, ha LINQ-t ír c#, 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 a System.Linq.Enumerable osztályban( például LINQ to Objects, LINQ to XML), a paraméter egy delegált típusú System.Func<T,TResult>. Amikor meghívja a Queryable.Select metódust a System.Linq.Queryable osztályban, például a LINQ-ban az SQL-ben, a paraméter típusa kifejezésfa típusú Expression<Func<TSource,TResult>>. Mindkét esetben ugyanazt a lambda kifejezést használhatja a paraméter értékének megadásához. Így a két Select hívás hasonlónak tűnik, 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 lambdakifejezé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 egy lekérdezésszolgáltató által kiértékelt kifejezésfákat hoz létre, korlátozza a metódushívásokat a lekérdezésszolgáltató által felismert metódusokra. Ellenkező esetben a lekérdezésszolgáltató nem tudja replikálni a metódus függvényét.

Kifejezés lambdák

A lambda utasítás hasonlít a lambda kifejezésre, de az utasítások kapcsos zárójelek között vannak.

(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;

A fordító általában következtet a lambda kifejezések paramétereinek típusaira, amit implicit típusú paraméterlistának nevezünk. A típusokat explicit módon, explicit módon beírt paraméterlistaként is megadhatja. Az alábbi példában egy explicit módon beírt paraméterlista látható. :

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. A C# 14 előtt meg kell adnia az explicit típust egy paraméterre, ha rendelkezik módosítókkal, például ref vagy out. A C# 14-ben ez a korlátozás el lesz távolítva. A módosító használata esetén azonban továbbra is deklarálnia kell a típust params .

A elvetések használatával megadhat két vagy több bemeneti paramétert egy lambda kifejezéshez, amelyek nem szerepelnek a kifejezésben:

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

A lambda dobás paraméterei akkor lehetnek hasznosak, ha lambdakifejezést használunk arra, hogy eseménykezelőt biztosítsunk.

Jegyzet

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

A C# 12-től kezdődően megadhat alapértelmezett értékeket a kifejezetten beírt paraméterlistákhoz. 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 vagy gyűjteményekkel rendelkező params Lambda-kifejezéseket is deklarálhatja a explicit módon beírt paraméterek listájának utolsó paramétereként:

var sum = (params IEnumerable<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 params gyűjteményparaméterrel rendelkező metóduscsoportok lambdakifejezéshez is hozzárendelhetők.

A paraméterekként alapértelmezett paraméterekkel vagy params gyűjteményekkel rendelkező Lambda-kifejezések nem rendelkeznek Func<> vagy Action<> típusnak megfelelő természetes típusokkal. 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);
delegate int SumCollectionDelegate(params IEnumerable<int> values);

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

További információ a lambdakifejezések alapértelmezett paramétereiről a "lambdakifejezések alapértelmezett paramétereinek" funkciós specifikációjában található.

Aszinkron lambda-kifejezések

Az aszinkron feldolgozást magukban foglaló lambdakifejezéseket és utasításokat egyszerűen létrehozhatja az async és await kulcsszavak használatával. A következő 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 vegyen fel egy async módosítót a lambda paraméterlista elé, 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 Aszinkron programozás aszinkronnal című témakörben talál, és várja meg.

Lambda-kifejezések és tupolok

A C# nyelv beépített támogatást nyújt a tuple-ökhöz . Lambda kifejezés argumentumaként megadhat egy n-es listát, és a lambda kifejezés vissza is adhat egy n-es listát. Bizonyos esetekben a C#-fordító típuskövetkeztetések használatával határozza meg a rekordelemek típusait.

A tömböt úgy határozhatja meg, hogy zárójelbe foglalja a vesszővel tagolt elemek 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)

A tuple mezői általában Item1, Item2stb. Azonban definiálhat nevesített összetevőkkel rendelkező tuplet, ahogyan az alábbi példa mutatja.

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

További információ a C#-n-esekről: N-es típusok.

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 általános meghatalmazottak Func<TResult> 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 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 delegált példányosítható Func<int, bool> példányként, ahol a int bemeneti paraméter, bool pedig a visszatérési érték. A visszatérési érték mindig az utolsó típusparaméterben van megadva. A Func<int, string, bool> például két bemeneti paraméterrel, int és string, valamint a boolvisszatérési típusával definiál egy delegáltat. A meghíváskor a következő Func delegált logikai értéket ad vissza, amely azt jelzi, hogy a bemeneti paraméter egyenlő-e öttel:

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 Queryable típusban definiált standard lekérdezési operátorokban. Amikor megad egy Expression<TDelegate> argumentumot, a lambda kifejezésfára lesz lefordítva.

Az alábbi példa a Count szabványos lekérdezési operátort 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 azokat az egész számokat (n) számolja meg, amelyek kettővel osztva maradékuk 1.

Az alábbi példa egy olyan sorozatot hoz létre, amely a numbers tömb minden olyan elemét tartalmazza, amely megelőzi a 9-et, mert 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 numbers tömb összes elemét visszaadja, amíg meg nem talál egy számot, 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 lekérdezési kifejezésekben, de a lekérdezési kifejezések metódushívásaiban is használhatja őket, ahogyan az alábbi példa 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

Típus-következtetés 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. Amennyiben egy IEnumerable<Customer>-t lekérdez, akkor a bemeneti változó egy Customer objektumként értelmeződik, ami azt jelenti, hogy elérheti annak metódusait és tulajdonságait.

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". Az informális "típus" arra a delegátustípusra vagy Expression típusra utal, amelyre a lambda kifejezés konvertálódik.

A lambda kifejezés rendelkezhet természetes típussal. Ahelyett, hogy kényszerítve lennél a delegált típus deklarálására, mint például Func<...> vagy Action<...> egy lambda kifejezéshez, a fordító képes kikövetkeztetni a delegált típusát a lambda kifejezés alapján. Vegyük például a következő deklarációt:

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

A fordító következtethet arra, hogy parse egy Func<string, int>. A fordító kiválaszt egy elérhető Func vagy Action delegálást, ha létezik megfelelő lehetőség. Ellenkező esetben delegált típust szintetizál. A delegálás típusa például akkor szintetizálódik, ha a lambda kifejezés ref paraméterekkel rendelkezik. Ha a lambda kifejezés természetes típusú, akkor kevésbé explicit típushoz rendelhető hozzá, például System.Object vagy System.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 hozzárendelsz egy lambda kifejezést a System.Linq.Expressions.LambdaExpressionvagy System.Linq.Expressions.Expressionhelyhez, és a lambda természetes delegálási típussal rendelkezik, a kifejezés a System.Linq.Expressions.Expression<TDelegate>természetes típusát veszi fel, ahol a típusparaméter argumentumaként a természetes delegálási 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 megállapítani 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 bemeneti paraméterek előtt megadhatja a lambda kifejezés visszatérési típusát. 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

Attribútumokat adhat hozzá a lambda kifejezéshez és 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 a meghívás eltér a metódusok és a helyi függvények függvényeinél. A meghatalmazott Invoke metódusa nem ellenőrzi a lambda kifejezés attribútumait. 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és nem alkalmazható.

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

A lambda kifejezések külső változókrahivatkozhatnak. Ezek a külső változók azok a változók, amelyek hatókörben vannak a lambda kifejezést meghatározó metódusban, vagy a lambda kifejezést tartalmazó típus hatókörében. 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ó nem kerül szemétgyűjtésre, amíg a rá hivatkozó delegált nem válik jogosulttá 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 körülvevő módszerből.
  • A lambda kifejezésben szereplő return utasítás nem eredményezi a körülvevő metódus visszatérését.
  • A lambda kifejezés nem tartalmazhat goto, break, vagy continue utasítást, ha az ugrási utasítás célja kívül esik a lambda kifejezés blokkján. Hiba, ha egy ugrási utasítás a lambda kifejezésblokkon kívül van, amikor a cél a blokkon belül található.

A static módosító a 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ókért lásd a Névtelen függvénykifejezések szakaszt a C# nyelvi specifikációban.

További információ ezekről a funkciókról a következő javasolt funkciójegyzetekben található:

Lásd még: