Лямбда-выражения и анонимные функции
Лямбда-выражение используется для создания анонимной функции. Используйте оператор объявления лямбда-выражения=>
для отделения списка параметров лямбда-выражения от исполняемого кода. Лямбда-выражение может иметь одну из двух следующих форм:
Лямбда выражения, имеющая выражение в качестве текста:
(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
Текст выражения лямбды может состоять из вызова метода. Но при создании деревьев выражений, которые вычисляются вне контекста поддержки общеязыковой среды выполнения (CRL) .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.
Вы можете использовать dis карта s для указания двух или нескольких входных параметров лямбда-выражения, которые не используются в выражении:
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");
Общие правила определения типа для лямбда-выражений формулируются следующим образом:
- лямбда-выражение должно содержать то же число параметров, что и тип делегата;
- каждый входной параметр в лямбда-выражении должен быть неявно преобразуемым в соответствующий параметр делегата;
- возвращаемое значение лямбда-выражения (если таковое имеется) должно быть неявно преобразуемым в возвращаемый тип делегата.
Естественный тип лямбда-выражения
Лямбда-выражение само по себе не имеет типа, так как система общих типов не имеет встроенной концепции "лямбда-выражения". Однако иногда удобно говорить о "типе" лямбда-выражения. Под неофициальным термином "тип" понимается тип делегата или тип Expression, в который преобразуется лямбда-выражение.
Начиная с C# 10, лямбда-выражение может иметь естественный тип. Вместо того, чтобы объявить тип делегата, например 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
Начиная с C# 10, можно указать тип возвращаемого значения лямбда-выражения перед входными параметрами. Если вы указываете явный тип возвращаемого значения, заключите входные параметры в скобки:
var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>
Атрибуты
Начиная с C# 10, вы можете добавлять атрибуты в лямбда-выражение и его параметры. В следующем примере показано, как добавить атрибуты в лямбда-выражение:
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
}
Следующие правила применимы к области действия переменной в лямбда-выражениях.
- Захваченная переменная не будет уничтожена сборщиком мусора до тех пор, пока делегат, который на нее ссылается, не перейдет в статус подлежащего уничтожению при сборке мусора.
- Переменные, представленные в лямбда-выражении, невидимы в заключающем методе.
- Лямбда-выражение не может непосредственно захватывать параметры in, ref или out из заключающего метода.
- Оператор return в лямбда-выражении не вызывает возврат значения заключающим методом.
- Лямбда-выражение не может содержать операторы goto, break или continue, если целевой объект этого оператора перехода находится за пределами блока лямбда-выражения. Если целевой объект находится внутри блока, использование оператора перехода за пределами лямбда-выражения также будет ошибкой.
Модификатор можно применить static
к лямбда-выражению, чтобы предотвратить непреднамеренный захват локальных переменных или состояния экземпляра лямбда-выражения:
Func<double, double> square = static x => x * x;
Статическое лямбда-лямбда-код не может записывать локальные переменные или состояние экземпляра из заключения область, но может ссылаться на статические элементы и определения констант.
Спецификация языка C#
Дополнительные сведения см. в разделе Выражения анонимных функций в спецификации языка C#.
Дополнительные сведения об этих функциях см. в следующих заметках о предложении функций: