Лямбда-выражения и анонимные функции
Для создания анонимной функции используется лямбда-выражение =>
, чтобы разделить список лямбда-параметров от его тела. Лямбда-выражение может иметь любой из следующих двух форм:
выражение лямбда- с выражением в качестве тела:
(input-parameters) => expression
оператор лямбда- с блоком инструкций в качестве тела:
(input-parameters) => { <sequence-of-statements> }
Чтобы создать лямбда-выражение, необходимо указать входные параметры (если таковые есть) слева от лямбда-оператора и выражения или блока инструкций на другой стороне.
Любое лямбда-выражение можно преобразовать в тип делегата . Типы его параметров и возвращаемое значение определяют тип делегата, в который можно преобразовать лямбда-выражение. Если лямбда-выражение не возвращает значение, его можно преобразовать в один из типов делегатов Action
; в противном случае его можно преобразовать в один из типов делегатов Func
. Например, лямбда-выражение, которое имеет два параметра и не возвращает значение, может быть преобразовано в делегат Action<T1,T2>. Лямбда-выражение, которое имеет один параметр и возвращает значение, можно преобразовать в делегат Func<T,TResult>. В следующем примере лямбда-выражение x => x * x
, которое задает параметр с именем x
и возвращает значение x
в квадрате, присваивается переменной типа делегат:
Func<int, int> square = x => x * x;
Console.WriteLine(square(5));
// Output:
// 25
Лямбда-выражения также можно преобразовать в типы дерева выражений , как показано в следующем примере:
System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
Console.WriteLine(e);
// Output:
// x => (x * x)
Вы используете лямбда-выражения в любом коде, который требует экземпляров типов делегатов или деревьев выражений. Одним из примеров является аргумент метода Task.Run(Action) для передачи кода, который должен выполняться в фоновом режиме. Также можно использовать лямбда-выражения при написании LINQ в C#, как показано в следующем примере:
int[] numbers = { 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(x => x * x);
Console.WriteLine(string.Join(" ", squaredNumbers));
// Output:
// 4 9 16 25
При использовании синтаксиса на основе методов для вызова метода Enumerable.Select в классе System.Linq.Enumerable, например в LINQ to Objects и LINQ to XML, параметр является типом делегата System.Func<T,TResult>. При вызове метода Queryable.Select в классе System.Linq.Queryable, например в LINQ to SQL, тип параметра — это тип дерева выражений Expression<Func<TSource,TResult>>
. В обоих случаях для указания значения параметра можно использовать одно и то же лямбда-выражение. Это делает два вызова Select
выглядеть похожими, хотя на самом деле тип объектов, созданных из лямбда-выражений, отличается.
Лямбда-выражения
Лямбда-выражение с выражением справа от оператора =>
называется лямбда-выражением . Лямбда-выражение возвращает результат выражения и принимает следующую базовую форму:
(input-parameters) => expression
Текст лямбда-выражения может состоять из вызова метода. Однако если вы создаете деревья выражений , которые оцениваются вне контекста среды CLR .NET, например в SQL Server, не следует использовать вызовы методов в лямбда-выражениях. Методы не имеют смысла вне контекста среды CLR .NET.
Лямбда-выражения инструкции
Лямбда-инструкция напоминает лямбда-выражение, за исключением того, что его операторы заключены в скобки:
(input-parameters) => { <sequence-of-statements> }
Текст лямбда-инструкции может состоять из любого количества инструкций; однако на практике обычно не более двух или трех.
Action<string> greet = name =>
{
string greeting = $"Hello {name}!";
Console.WriteLine(greeting);
};
greet("World");
// Output:
// Hello World!
Нельзя использовать лямбда-выражения для создания деревьев выражений.
Входные параметры лямбда-выражения
Входные параметры лямбда-выражения заключены в скобки. Укажите нулевые входные параметры с пустыми скобками:
Action line = () => Console.WriteLine();
Если лямбда-выражение имеет только один входной параметр, скобки являются необязательными:
Func<double, double> cube = x => x * x * x;
Два или более входных параметров разделены запятыми:
Func<int, int, bool> testForEquality = (x, y) => x == y;
Иногда компилятор не может выводить типы входных параметров. Вы можете явно указать типы, как показано в следующем примере:
Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;
Типы входных параметров должны быть явными или неявными; в противном случае возникает ошибка компилятора CS0748.
Можно использовать отменять, чтобы указать два или более входных параметров лямбда-выражения, которые не используются в выражении:
Func<int, int, int> constant = (_, _) => 42;
Параметры лямбда-отмены могут быть полезны при использовании лямбда-выражения для предоставления обработчика событий.
Заметка
Для обратной совместимости, если только один входной параметр называется _
, то в лямбда-выражении _
рассматривается как имя этого параметра.
Начиная с C# 12, можно указать значения по умолчанию, для параметров в лямбда-выражениях. Синтаксис и ограничения значений параметров по умолчанию совпадают с методами и локальными функциями. В следующем примере объявляется лямбда-выражение с параметром по умолчанию, а затем вызывает его один раз с использованием значения по умолчанию и один раз с двумя явными параметрами:
var IncrementBy = (int source, int increment = 1) => source + increment;
Console.WriteLine(IncrementBy(5)); // 6
Console.WriteLine(IncrementBy(5, 2)); // 7
Можно также определить лямбда-выражения с массивами или коллекциями params
как параметры:
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
В рамках этих обновлений, когда группе методов, которая имеет параметр по умолчанию, назначается лямбда-выражение, это лямбда-выражение также имеет тот же параметр по умолчанию. Группу методов с параметром коллекции params
также можно привязать к лямбда-выражению.
Лямбда-выражения с параметрами по умолчанию или коллекциями params
в качестве параметров не имеют естественных типов, соответствующих Func<>
или Action<>
типам. Однако можно определить типы делегатов, которые включают значения параметров по умолчанию:
delegate int IncrementByDelegate(int source, int increment = 1);
delegate int SumDelegate(params int[] values);
delegate int SumCollectionDelegate(params IEnumerable<int> values);
Кроме того, можно использовать неявно типизированные переменные с декларациями на основе var
для определения типа делегата. Компилятор синтезирует правильный тип делегата.
Дополнительные сведения о параметрах по умолчанию для лямбда-выражений можно найти в спецификации возможностей на параметры по умолчанию для лямбда-выражений .
Асинхронные лямбда-выражения
Вы можете легко создавать лямбда-выражения и инструкции, которые включают асинхронную обработку, используя ключевые слова async и await. Например, следующий пример Windows Forms содержит обработчик событий, который вызывает и ожидает асинхронный метод, 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);
}
}
Вы можете добавить один и тот же обработчик событий с помощью асинхронного лямбда-кода. Чтобы добавить этот обработчик, добавьте модификатор async
перед списком лямбда-параметров, как показано в следующем примере:
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);
}
}
Дополнительные сведения о создании и использовании асинхронных методов см. в Асинхронное программирование с async и await.
Лямбда-выражения и кортежи
Язык C# обеспечивает встроенную поддержку кортежей кортежей. Кортеж можно предоставить в качестве аргумента лямбда-выражения, а лямбда-выражение также может возвращать кортеж. В некоторых случаях компилятор C# использует вывод типов данных для определения типов компонентов кортежа.
Вы определяете кортеж, заключив список его компонентов с разделителями-запятыми в скобках. В следующем примере кортеж с тремя компонентами используется для передачи последовательности чисел лямбда-выражению, которое удвоит каждое значение и возвращает кортеж с тремя компонентами, содержащими результат умножения.
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)
Обычно поля кортежа называются Item1
, Item2
и т. д. Однако можно определить кортеж с именованными компонентами, как показано в следующем примере.
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}");
Дополнительные сведения о кортежах C# см. в типах кортежей.
Лямбда со стандартными операторами запросов
LINQ to Objects, среди других реализаций, имеет входной параметр, тип которого является одним из Func<TResult> семейства универсальных делегатов. Эти делегаты используют параметры типа для определения числа и типа входных параметров, а также возвращаемого типа делегата.
Func
делегаты полезны для инкапсулирования определяемых пользователем выражений, применяемых к каждому элементу в наборе исходных данных. Например, рассмотрим тип делегата Func<T,TResult>:
public delegate TResult Func<in T, out TResult>(T arg)
Делегат можно создать как экземпляр Func<int, bool>
, где int
является входным параметром, а bool
— возвращаемым значением. Возвращаемое значение всегда указывается в последнем параметре типа. Например, Func<int, string, bool>
определяет делегат с двумя входными параметрами, int
и string
и возвращаемым типом bool
. Следующий Func
делегат при вызове возвращает логическое значение, указывающее, равен ли входной параметр пяти:
Func<int, bool> equalsFive = x => x == 5;
bool result = equalsFive(4);
Console.WriteLine(result); // False
Можно также указать лямбда-выражение, если тип аргумента является Expression<TDelegate>, например в стандартных операторах запросов, определенных в типе Queryable. Когда указываете аргумент Expression<TDelegate>, лямбда компилируется в дерево выражений.
В следующем примере используется стандартный оператор запроса Count:
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)}");
Компилятор может определить тип входного параметра или явно указать его. Это конкретное лямбда-выражение подсчитывает эти целые числа (n
), которые при делении на два имеют оставшуюся часть 1.
В следующем примере создается последовательность, содержащая все элементы в массиве numbers
, предшествующие 9, так как это первое число в последовательности, которая не соответствует условию:
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
В следующем примере указывается несколько входных параметров, заключив их в скобки. Метод возвращает все элементы в массиве numbers
, пока не обнаружит число, значение которого меньше его порядкового положения в массиве:
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
Лямбда-выражения не используются непосредственно в выражениях запросов , но их можно использовать в вызовах методов в выражениях запросов, как показано в следующем примере:
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
Вывод типов в лямбда-выражениях
При написании лямбда-кодов часто не нужно указывать тип входных параметров, так как компилятор может выводить тип на основе лямбда-текста, типов параметров и других факторов, как описано в спецификации языка C#. Для большинства стандартных операторов запросов первый вход — это тип элементов в исходной последовательности. Если вы запрашиваете IEnumerable<Customer>
, то входная переменная выводится в виде объекта Customer
, что означает, что у вас есть доступ к его методам и свойствам:
customers.Where(c => c.City == "London");
Общие правила вывода типов для лямбда-кодов приведены следующим образом:
- Лямбда-код должен содержать то же количество параметров, что и тип делегата.
- Каждый входной параметр в лямбда-выражении должен быть неявно преобразуем в соответствующий параметр делегата.
- Возвращаемое значение лямбда-выражения (если оно есть) должно быть неявно преобразуемо в тип возвращаемого значения делегата.
Естественный тип лямбда-выражения
Лямбда-выражение само по себе не имеет типа, так как система общих типов не имеет встроенной концепции "лямбда-выражения". Однако иногда удобно говорить о "типе" лямбда-выражения. Этот неформальный "type" относится к типу делегата или типу Expression, в который преобразуется лямбда-выражение.
Лямбда-выражение может иметь естественный тип. Вместо принудительного объявления типа делегата, например Func<...>
или Action<...>
для лямбда-выражения, компилятор может выводить тип делегата из лямбда-выражения. Например, рассмотрим следующее объявление:
var parse = (string s) => int.Parse(s);
Компилятор может сделать вывод, что parse
является Func<string, int>
. Компилятор выбирает доступный делегат Func
или Action
, если он подходит. В противном случае система синтезирует тип делегата. Например, если лямбда-выражение имеет параметры ref
, тип делегата синтезируется. Если лямбда-выражение имеет естественный тип, его можно назначить менее явному типу, например System.Object или System.Delegate:
object parse = (string s) => int.Parse(s); // Func<string, int>
Delegate parse = (string s) => int.Parse(s); // Func<string, int>
Группы методов (то есть имена методов без списков параметров) с точно одной перегрузкой имеют естественный тип:
var read = Console.Read; // Just one overload; Func<int> inferred
var write = Console.Write; // ERROR: Multiple overloads, can't choose
При назначении лямбда-выражения на System.Linq.Expressions.LambdaExpressionили System.Linq.Expressions.Expression, и если лямбда-выражение имеет естественный тип делегата, выражение имеет естественный тип System.Linq.Expressions.Expression<TDelegate>, с естественным типом делегата, используемым в качестве аргумента для параметра типа.
LambdaExpression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Expression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Не все лямбда-выражения имеют естественный тип. Рассмотрим следующее объявление:
var parse = s => int.Parse(s); // ERROR: Not enough type info in the lambda
Компилятор не может определить тип параметра для s
. Если компилятор не может вывести естественный тип, необходимо объявить тип:
Func<string, int> parse = s => int.Parse(s);
Явный тип возвращаемого значения
Как правило, возвращаемый тип лямбда-выражения очевиден и выведен. Для некоторых выражений, которые не работают:
var choose = (bool b) => b ? 1 : "two"; // ERROR: Can't infer return type
Вы можете указать тип возвращаемого значения лямбда-выражения перед входными параметрами. При указании явного типа возвращаемого значения необходимо скобить входные параметры:
var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>
Атрибуты
Атрибуты можно добавить в лямбда-выражение и его параметры. В следующем примере показано, как добавить атрибуты в лямбда-выражение:
Func<string?, int?> parse = [ProvidesNullCheck] (s) => (s is not null) ? int.Parse(s) : null;
Вы также можете добавить атрибуты в входные параметры или возвращаемое значение, как показано в следующем примере:
var concat = ([DisallowNull] string a, [DisallowNull] string b) => a + b;
var inc = [return: NotNullIfNotNull(nameof(s))] (int? s) => s.HasValue ? s++ : null;
Как показано в предыдущих примерах, при добавлении атрибутов в лямбда-выражение или его параметры необходимо скобить входные параметры.
Важный
Лямбда-выражения вызываются через базовый тип делегата. Это отличается от методов и локальных функций. Метод Invoke
делегата не проверяет атрибуты в лямбда-выражении. Атрибуты не имеют никакого эффекта при вызове лямбда-выражения. Атрибуты лямбда-выражений полезны для анализа кода и могут быть обнаружены с помощью отражения. Одним из последствий этого решения является то, что System.Diagnostics.ConditionalAttribute нельзя применить к лямбда-выражению.
Захват внешних переменных и области переменных в лямбда-выражениях
Лямбда-выражения могут ссылаться на внешних переменных. Эти внешние переменные являются переменными, которые находятся в области видимости метода, который определяет лямбда-выражение, или в области видимости типа, содержащего лямбда-выражение. Переменные, захваченные таким образом, хранятся для использования в лямбда-выражении даже в том случае, если они в противном случае вышли бы из области видимости и стали бы доступны для сборки мусора. Перед использованием внешней переменной в лямбда-выражении ее необходимо предварительно присвоить. В следующем примере показаны следующие правила:
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
}
Следующие правила применяются к области переменной в лямбда-выражениях:
- Захваченная переменная не подвергается сборке мусора до тех пор, пока делегат, ссылающийся на неё, не станет подлежащим сборке мусора.
- Переменные, введенные в лямбда-выражение, не отображаются в заключаемом методе.
- Лямбда-выражение не может напрямую захватывать параметр в, refпараметр или out параметр из охватывающего метода.
- Инструкция возврата в лямбда-выражении не приводит к возврату включающего метода.
- Лямбда-выражение не может содержать goto, breakили continue оператор, если цель этого оператора перехода находится вне блока лямбда-выражения. Это также ошибка иметь инструкцию перехода за пределами блока лямбда-выражений, если целевой объект находится внутри блока.
Модификатор static
можно применить к лямбда-выражению, чтобы предотвратить непреднамеренный захват локальных переменных или состояния экземпляра лямбда-выражения:
Func<double, double> square = static x => x * x;
Статическая лямбда-выражение не может захватывать локальные переменные или состояние экземпляра из внешних областей видимости, но может ссылаться на статические члены и определения констант.
Спецификация языка C#
Дополнительные сведения см. в разделе Анонимные выражения функций спецификации языка C#.
Дополнительные сведения об этих функциях см. в следующих заметках о предложении функций: