Výrazy lambda a anonymní funkce
K vytvoření anonymní funkce použijete výraz lambda. Pomocí operátoru deklarace lambda =>
oddělte seznam parametrů lambda od jejího těla. Výraz lambda může být z některé z následujících dvou forem:
výraz lambda, který má výraz jako text:
(input-parameters) => expression
výraz lambda, který má blok příkazu jako jeho text:
(input-parameters) => { <sequence-of-statements> }
Pokud chcete vytvořit výraz lambda, zadáte vstupní parametry (pokud existuje) na levé straně operátoru lambda a výraz nebo blok příkazu na druhé straně.
Libovolný výraz lambda lze převést na typ delegáta. Typy parametrů a návratová hodnota definují typ delegáta, na který lze výraz lambda převést. Pokud výraz lambda nevrací hodnotu, může být převeden na jeden z typů delegátů Action
; v opačném případě je možné jej převést na jeden z typů delegátů Func
. Například výraz lambda, který má dva parametry a nevrací žádnou hodnotu, lze převést na delegáta Action<T1,T2>. Výraz lambda, který má jeden parametr a vrací hodnotu, lze převést na Func<T,TResult> delegáta. V následujícím příkladu je výraz lambda x => x * x
, který určuje parametr s názvem x
a vrací hodnotu x
čtverce, je přiřazena proměnné typu delegáta:
Func<int, int> square = x => x * x;
Console.WriteLine(square(5));
// Output:
// 25
Jak ukazuje následující příklad, výrazy lambda lze také převést na typy stromu výrazů .
System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
Console.WriteLine(e);
// Output:
// x => (x * x)
Výrazy lambda se používají v libovolném kódu, který vyžaduje instance typů delegátů nebo stromů výrazů. Jedním z příkladů je argument metody Task.Run(Action) pro předání kódu, který by měl být proveden na pozadí. Výrazy lambda můžete použít také při psaní LINQ v jazyce C#, jak ukazuje následující příklad:
int[] numbers = { 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(x => x * x);
Console.WriteLine(string.Join(" ", squaredNumbers));
// Output:
// 4 9 16 25
Pokud používáte syntaxi založenou na metodách k volání metody Enumerable.Select ve třídě System.Linq.Enumerable, například v LINQ to Objects a LINQ to XML, je parametr typu delegáta System.Func<T,TResult>. Když voláte metodu Queryable.Select ve třídě System.Linq.Queryable, typ parametru je strom výrazu Expression<Func<TSource,TResult>>
, například v LINQ to SQL. V obou případech můžete k zadání hodnoty parametru použít stejný výraz lambda. To způsobuje, že dvě Select
volání vypadají podobně, ačkoli ve skutečnosti je typ objektů, které jsou vytvořeny z lambd, odlišný.
Výrazy lambda
Výraz lambda s výrazem na pravé straně operátoru =>
se nazývá výraz lambda . Výraz lambda vrátí výsledek výrazu a má následující základní formu:
(input-parameters) => expression
Tělo výrazu lambda se může skládat z volání metody. Pokud ale vytváříte stromy výrazů , které se vyhodnocují mimo kontext modulu CLR (Common Language Runtime), například v SQL Serveru, neměli byste ve výrazech lambda používat volání metod. Metody nemají žádný význam mimo kontext modulu CLR (Common Language Runtime).
Výroková lambda
Příkaz lambda se podobá výrazu lambda, ale jeho příkazy jsou uzavřeny ve složených závorkách:
(input-parameters) => { <sequence-of-statements> }
Tělo výrazu lambda se může skládat z jakéhokoliv počtu příkazů; v praxi se však obvykle neskládá více než ze dvou nebo tří.
Action<string> greet = name =>
{
string greeting = $"Hello {name}!";
Console.WriteLine(greeting);
};
greet("World");
// Output:
// Hello World!
Výrazy lambda nemůžete použít k vytvoření stromů výrazů.
Vstupní parametry výrazu lambda
Vstupní parametry výrazu lambda uzavřete do závorek. Zadejte nulové vstupní parametry s prázdnými závorky:
Action line = () => Console.WriteLine();
Pokud výraz lambda obsahuje pouze jeden vstupní parametr, jsou závorky volitelné:
Func<double, double> cube = x => x * x * x;
Dva nebo více vstupních parametrů jsou oddělené čárkami:
Func<int, int, bool> testForEquality = (x, y) => x == y;
Kompilátor někdy nemůže odvodit typy vstupních parametrů. Typy můžete zadat explicitně, jak je znázorněno v následujícím příkladu:
Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;
Vstupní typy parametrů musí být explicitní nebo všechny implicitní; jinak dojde k chybě kompilátoru CS0748.
Pomocí zahození můžete zadat dva nebo více vstupních parametrů lambda výrazu, které se ve výrazu nepoužívají.
Func<int, int, int> constant = (_, _) => 42;
Parametry zahození lambda mohou být užitečné při použití výrazu lambda k poskytnutí obslužné rutiny události.
Poznámka
Pro zpětnou kompatibilitu platí, že pokud má _
název pouze jednoho vstupního parametru, pak se v rámci výrazu lambda _
považuje za název tohoto parametru.
Od C# 12 můžete zadávat výchozí hodnoty parametrů ve výrazech lambda. Syntaxe a omezení výchozích hodnot parametrů jsou stejné jako u metod a místních funkcí. Následující příklad deklaruje výraz lambda s výchozím parametrem a potom ho volá jednou pomocí výchozího a jednou se dvěma explicitními parametry:
var IncrementBy = (int source, int increment = 1) => source + increment;
Console.WriteLine(IncrementBy(5)); // 6
Console.WriteLine(IncrementBy(5, 2)); // 7
Výrazy lambda můžete deklarovat také pomocí polí params
nebo kolekcí jako parametrů:
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
V rámci těchto aktualizací, když je skupině metod, která má výchozí parametr, přiřazena lambda výraz, tento lambda výraz má také stejný výchozí parametr. Skupinu metod s parametrem kolekce params
lze také přiřadit výrazu lambda.
Výrazy lambda s výchozími parametry nebo kolekcemi params
jako parametry nemají přirozené typy, které odpovídají Func<>
nebo Action<>
typům. Můžete ale definovat typy delegátů, které obsahují výchozí hodnoty parametrů:
delegate int IncrementByDelegate(int source, int increment = 1);
delegate int SumDelegate(params int[] values);
delegate int SumCollectionDelegate(params IEnumerable<int> values);
Nebo můžete k definování typu delegáta použít implicitně zadané proměnné s deklaracemi var
. Kompilátor syntetizuje správný typ delegáta.
Další informace o výchozích parametrech pro výrazy lambda naleznete ve specifikaci funkce pro výchozí parametry výrazů lambda.
Asynchronní lambda
Snadno můžete vytvářet lambda výrazy a příkazy, které zahrnují asynchronní zpracování, pomocí klíčových slov async a await. Například následující příklad Windows Forms obsahuje obslužnou rutinu události, která volá a očekává asynchronní metodu 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);
}
}
Stejnou obslužnou rutinu události můžete přidat pomocí asynchronního lambda. Chcete-li přidat tuto obslužnou rutinu, přidejte před seznam parametrů lambda modifikátor async
, jak ukazuje následující příklad:
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);
}
}
Další informace o tom, jak vytvářet a používat asynchronní metody, naleznete v tématu Asynchronní programování pomocí async a await.
Výrazy lambda a n-tice
Jazyk C# poskytuje integrovanou podporu pro n-tice. Jako argument funkce lambda můžete zadat n-tici a funkce lambda může také vrátit n-tici. V některých případech kompilátor jazyka C# používá odvození typu k určení typů součástí n-tice.
Řazenou kolekci členů definujete uzavřením čárkami odděleného seznamu jejích součástí do závorek. Následující příklad používá řazenou kolekci členů se třemi komponentami k předání posloupnosti čísel výrazu lambda, který zdvojnásobí každou hodnotu a vrátí řazenou kolekci členů se třemi komponentami, které obsahují výsledek násobení.
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)
Obvykle se pole n-tice nazývají Item1
, Item2
atd. Můžete však definovat řazenou kolekci členů s pojmenovanými komponentami, jak to dělá následující příklad.
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}");
Další informace o n-ticích v C# najdete v tématu Typy n-tic.
Lambdas se standardními operátory dotazů
LINQ to Objects, mezi jinými implementacemi, má vstupní parametr, jehož typ patří do rodiny obecných delegátů Func<TResult>. Tito delegáti používají parametry typu k definování počtu a typu vstupních parametrů a návratového typu delegáta.
Func
delegáti jsou užiteční pro zapouzdření výrazů definovaných uživatelem, které se aplikují na každý prvek v sadě zdrojových dat. Představte si například typ delegáta Func<T,TResult>:
public delegate TResult Func<in T, out TResult>(T arg)
Delegáta lze vytvořit jako instanci Func<int, bool>
, kde int
je vstupní parametr a bool
je návratová hodnota. Vrácená hodnota je vždy zadána v posledním parametru typu. Například Func<int, string, bool>
definuje delegáta se dvěma vstupními parametry, int
a string
a návratovým typem bool
. Následující Func
delegát při vyvolání vrátí logickou hodnotu, která označuje, zda je vstupní parametr roven pěti:
Func<int, bool> equalsFive = x => x == 5;
bool result = equalsFive(4);
Console.WriteLine(result); // False
Výraz lambda můžete také zadat, pokud je typ argumentu Expression<TDelegate>, například ve standardních operátorech dotazu, které jsou definovány v Queryable typu. Když zadáte Expression<TDelegate> argument, lambda se zkompiluje do stromu výrazů.
Následující příklad používá Count standardní operátor dotazu:
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)}");
Kompilátor může odvodit typ vstupního parametru nebo ho můžete zadat explicitně. Tento konkrétní výraz lambda spočítá celá čísla (n
), které při dělení dvěma mají zbytek 1.
Následující příklad vytvoří sekvenci, která obsahuje všechny prvky v poli numbers
, která předchází 9, protože to je první číslo v posloupnosti, která nesplňuje podmínku:
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
Následující příklad určuje více vstupních parametrů uzavřením do závorek. Metoda vrátí všechny prvky v numbers
matice, dokud nenajde číslo, jehož hodnota je menší než jeho pořadová pozice v matici:
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
Výrazy lambda nepoužíváte přímo ve výrazech dotazu , ale můžete je použít ve volání metod ve výrazech dotazu, jak ukazuje následující příklad:
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
Odvození typu ve výrazech lambda
Při psaní lambda často nemusíte zadávat typ vstupních parametrů, protože kompilátor může typ odvodit na základě těla lambda, typů parametrů a dalších faktorů popsaných ve specifikaci jazyka C#. U většiny standardních operátorů dotazu je prvním vstupem typ prvků ve zdrojové sekvenci. Pokud dotazujete IEnumerable<Customer>
, vstupní proměnná se odvozuje jako objekt Customer
, což znamená, že máte přístup k jeho metodám a vlastnostem:
customers.Where(c => c.City == "London");
Obecná pravidla pro odvození typu pro lambda jsou následující:
- Lambda musí obsahovat stejný počet parametrů jako typ delegáta.
- Každý vstupní parametr v lambda musí být implicitně konvertibilní na odpovídající parametr delegáta.
- Návratová hodnota lambda (pokud existuje) musí být implicitně konvertibilní na návratový typ delegáta.
Přirozený typ výrazu lambda
Výraz lambda sám o sobě nemá typ, protože systém běžných typů nemá žádný vnitřní koncept výrazu lambda. Někdy je ale vhodné mluvit neformálně o "typu" výrazu lambda. Tento neformální "typ" odkazuje na typ delegáta nebo Expression typ, na který je výraz lambda převeden.
Výraz lambda může mít přirozený typ. Místo nutnosti deklarovat typ delegáta, například Func<...>
nebo Action<...>
pro výraz lambda, může kompilátor odvodit typ delegáta z výrazu lambda. Představte si například následující deklaraci:
var parse = (string s) => int.Parse(s);
Kompilátor může odvodit, že parse
je Func<string, int>
. Kompilátor zvolí dostupného delegáta Func
nebo Action
, pokud je k dispozici vhodný. V opačném případě syntetizuje typ delegáta. Například typ delegáta je syntetizován, pokud výraz lambda má ref
parametry. Pokud má výraz lambda přirozený typ, může být přiřazen méně explicitnímu typu, například System.Object nebo System.Delegate:
object parse = (string s) => int.Parse(s); // Func<string, int>
Delegate parse = (string s) => int.Parse(s); // Func<string, int>
Skupiny metod (to znamená názvy metod bez seznamů parametrů) s přesně jedním přetížením mají přirozený typ:
var read = Console.Read; // Just one overload; Func<int> inferred
var write = Console.Write; // ERROR: Multiple overloads, can't choose
Pokud přiřadíte výraz lambda k System.Linq.Expressions.LambdaExpressionnebo System.Linq.Expressions.Expressiona lambda má přirozený typ delegáta, výraz má přirozený typ System.Linq.Expressions.Expression<TDelegate>, s typem přirozeného delegáta použitým jako argument parametru typu:
LambdaExpression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Expression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Ne všechny výrazy lambda mají přirozený typ. Představte si následující deklaraci:
var parse = s => int.Parse(s); // ERROR: Not enough type info in the lambda
Kompilátor nemůže odvodit typ parametru pro s
. Pokud kompilátor nemůže odvodit přirozený typ, musíte deklarovat typ:
Func<string, int> parse = s => int.Parse(s);
Explicitní návratový typ
Návratový typ výrazu lambda je obvykle zřejmý a odvozený. U některých výrazů, které nefungují:
var choose = (bool b) => b ? 1 : "two"; // ERROR: Can't infer return type
Před vstupními parametry můžete zadat návratový typ výrazu lambda. Pokud zadáte explicitní návratový typ, je nutné dát vstupní parametry do závorek.
var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>
Atributy
Do výrazu lambda a jeho parametrů můžete přidat atributy. Následující příklad ukazuje, jak přidat atributy do výrazu lambda:
Func<string?, int?> parse = [ProvidesNullCheck] (s) => (s is not null) ? int.Parse(s) : null;
Můžete také přidat atributy ke vstupním parametrům nebo návratové hodnotě, jak ukazuje následující příklad:
var concat = ([DisallowNull] string a, [DisallowNull] string b) => a + b;
var inc = [return: NotNullIfNotNull(nameof(s))] (int? s) => s.HasValue ? s++ : null;
Jak ukazují předchozí příklady, je nutné uzavřít vstupní parametry do závorek, když přidáváte atributy k lambda výrazu nebo jeho parametrům.
Důležitý
Výrazy lambda se vyvolávají prostřednictvím základního typu delegáta. To se liší od metod a místních funkcí. Metoda Invoke
delegáta nekontroluje atributy výrazu lambda. Atributy nemají žádný vliv při vyvolání výrazu lambda. Atributy výrazů lambda jsou užitečné pro analýzu kódu a lze je zjistit prostřednictvím reflexe. Jedním z důsledků tohoto rozhodnutí je, že System.Diagnostics.ConditionalAttribute nelze použít u výrazu lambda.
Zachycení vnějších proměnných a rozsahu proměnných ve výrazech lambda
Lambdy mohou odkazovat na vnější proměnné. Tyto vnější proměnné jsou proměnné, které jsou ve scope metody, která definuje výraz lambda, nebo ve scope typu, který výraz lambda obsahuje. Proměnné zachycené tímto způsobem se ukládají pro použití ve výrazu lambda, i když by proměnné jinak přešly mimo rozsah a byly uvolněny z paměti. Vnější proměnná musí být jednoznačně přiřazená, aby ji bylo možné použít ve výrazu lambda. Následující příklad ukazuje tato pravidla:
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
}
Následující pravidla platí pro dosah proměnných ve výrazech lambda:
- Proměnná, která je zachycena, se neshromažďuje, dokud delegát, který na něj odkazuje, nebude způsobilý pro uvolňování paměti.
- Proměnné zavedené ve výrazu lambda nejsou viditelné v ohraničující metodě.
- Výraz lambda nemůže přímo zachytit v, refnebo out parametr z uzavírající metody.
- Příkaz return ve výrazu lambda nezpůsobí, že metoda, která jej obsahuje, vrátí hodnotu.
- Výraz lambda nemůže obsahovat příkazy goto, breaknebo continue, pokud cíl těchto skokových příkazů je mimo blok výrazu lambda. Je také chybou mít příkaz skoku mimo blok lambda výrazu, pokud je cíl uvnitř bloku.
Modifikátor static
můžete použít u výrazu lambda, aby se zabránilo neúmyslnému zachycení místních proměnných nebo stavu instance lambda:
Func<double, double> square = static x => x * x;
Statická lambda nemůže zachytit místní proměnné nebo stav instance z uzavřených oborů, ale může odkazovat na statické členy a definice konstant.
Specifikace jazyka C#
Další informace najdete v části Anonymní výrazy funkce specifikace jazyka jazyka C#.
Další informace o těchto funkcích najdete v následujících poznámkách k návrhu funkcí: