Поделиться через


Лямбда-выражения и анонимные функции

Для создания анонимной функции используется лямбда-выражение . Используйте оператор лямбда-объявления =>, чтобы разделить список лямбда-параметров от его тела. Лямбда-выражение может иметь любой из следующих двух форм:

Чтобы создать лямбда-выражение, необходимо указать входные параметры (если таковые есть) слева от лямбда-оператора и выражения или блока инструкций на другой стороне.

Любое лямбда-выражение можно преобразовать в тип делегата . Типы его параметров и возвращаемое значение определяют тип делегата, в который можно преобразовать лямбда-выражение. Если лямбда-выражение не возвращает значение, его можно преобразовать в один из типов делегатов 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#.

Дополнительные сведения об этих функциях см. в следующих заметках о предложении функций:

См. также