Лямбда-выражения (Руководство по программированию в C#)

Лямбда-выражение — это анонимная функция, с помощью которой можно создавать типы делегатов или деревьев выражений. С помощью лямбда-выражений можно писать локальные функции, которые можно передавать в качестве аргументов или возвращать в качестве значений из вызовов функций. Лямбда-выражения особенно полезны при написании выражений запросов LINQ.

Чтобы создать лямбда-выражение, необходимо указать входные параметры (если они есть) с левой стороны лямбда-оператора =>, и поместить блок выражений или операторов с другой стороны. Например, лямбда-выражение x => x * x задает параметр с именем x и возвращает значение x. Можно назначить это выражение типу делегата, как показано в следующем примере:

delegate int del(int i);
static void Main(string[] args)
{
    del myDelegate = x => x * x;
    int j = myDelegate(5); //j = 25
}

Создание типа дерева выражений:

using System.Linq.Expressions;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Expression<del> myET = x => x * x;
        }
    }
}

Оператор => имеет такой же приоритет, как и присваивание (=), и является правоассоциативным (см. раздел "Ассоциативность" статьи об операторах).

Лямбда-операторы используются в запросах LINQ на основе методов в качестве аргументов стандартных методов операторов запроса, таких как Where``1.

При использовании синтаксиса на основе методов для вызова метода Where``1 в классе Enumerable (как это делается в LINQ на объекты и LINQ to XML) параметром является тип делегата Func. Лямбда-выражение — это наиболее удобный способ создания делегата. При вызове того же метода, к примеру, в классе Queryable (как это делается в LINQ to SQL) типом параметра будет Expression<Func>, где Func — это любые делегаты Func с числом входных параметров не более шестнадцати. Опять же, лямбда-выражения представляют собой самый быстрый способ построения дерева выражений. Лямбда-выражения позволяют вызовам Where выглядеть одинаково, хотя на самом деле объект, созданный из лямбда-выражения, имеет другой тип.

Обратите внимание: в приведенном выше примере сигнатура делегата имеет один неявный входной параметр типа int и возвращает значение типа int. Лямбда-выражение можно преобразовать в делегат соответствующего типа, поскольку он также имеет один входной параметр (x) и возвращает значение, которое компилятор может неявно преобразовать в тип int. (Вывод типов более подробно рассматривается в следующих разделах.) Делегат, вызываемый посредством входного параметра 5, возвращает результат 25.

Лямбда-выражения нельзя использовать с левой стороны оператора is или as.

Все ограничения, применяемые к анонимным методам, применяются также к лямбда-выражениям. Дополнительные сведения см. в разделе Анонимные методы (Руководство по программированию в C#).

Выражения-лямбды

Лямбда-выражение с выражением с правой стороны оператора => называется выражением-лямбдой. Выражения-лямбды широко используются при конструировании Деревья выражений (C# и Visual Basic). Выражения-лямбды возвращают результат выражения и принимают следующую основную форму.

(input parameters) => expression

Если лямбда-выражение имеет только один входной параметр, скобки можно не ставить; во всех остальных случаях они обязательны. Два и более входных параметра разделяются запятыми и заключаются в скобки:

(x, y) => x == y

Иногда компилятору бывает трудно или даже невозможно определить входные типы. В этом случае типы можно указать в явном виде, как показано в следующем примере.

(int x, string s) => s.Length > x

Нулевое количество входных параметры задается пустыми скобками:

() => SomeMethod()

Обратите внимание, что тело выражения-лямбды может состоять из вызова метода, как было показано в предыдущем примере. Однако при создании деревьев выражений, которые вычисляются вне .NET Framework, например в SQL Server, не следует использовать вызовы методов в лямбда-выражениях. Эти методы не имеют смысла вне контекста среды CLR .NET.

Лямбды операторов

Лямбда оператора напоминает выражение-лямбду, за исключением того, что оператор (или операторы) заключается в фигурные скобки:

(input parameters) => {statement;}

Тело лямбды оператора может состоять из любого количества операторов; однако на практике обычно используется не более двух-трех.

delegate void TestDelegate(string s);
…
TestDelegate myDel = n => { string s = n + " " + "World"; Console.WriteLine(s); };
myDel("Hello");

Лямбды операторов, как и анонимные методы, не могут использоваться для создания деревьев выражений.

Асинхронные лямбда-выражения

С помощью ключевых слов async и await можно легко создавать лямбда-выражения и операторы, включающие асинхронную обработку. Например, в следующем примере Windows Forms содержится обработчик событий, который вызывает асинхронный метод ExampleMethodAsync и ожидает его.

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private async void button1_Click(object sender, EventArgs e)
    {
        // ExampleMethodAsync returns a Task.
        await ExampleMethodAsync();
        textBox1.Text += "\r\nControl returned to Click event handler.\r\n";
    }

    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) =>
        {
            // ExampleMethodAsync returns a Task.
            await ExampleMethodAsync();
            textBox1.Text += "\r\nControl returned to Click event handler.\r\n";
        };
    }

    async Task ExampleMethodAsync()
    {
        // The following line simulates a task-returning asynchronous process.
        await Task.Delay(1000);
    }
}

Дополнительные сведения о создании и использовании асинхронных методов см. в разделе Асинхронное программирование с использованием ключевых слов Async и Await (C# и Visual Basic);

Лямбды со стандартными операторами запросов

Многие стандартные операторы запросов имеют входной параметр, тип которого принадлежит к семейству Func универсальных делегатов. Эти делегаты используют параметры типа для определения количества и типов входных параметров, а также тип возвращаемого значения делегата. Делегаты Func очень полезны для инкапсуляции пользовательских выражений, которые применяются к каждому элементу в наборе исходных данных. В качестве примера рассмотрим следующий тип делегата.

public delegate TResult Func<TArg0, TResult>(TArg0 arg0)

Экземпляр этого делегата можно создать как Func<int,bool> myFunc, где int — входной параметр, а bool — возвращаемое значение. Возвращаемое значение всегда указывается в последнем параметре типа. Func<int, string, bool> определяет делегат с двумя входными параметрами, int и string, и типом возвращаемого значения bool. Следующий делегат Func при вызове возвращает значение true или false, которое показывает, равен ли входной параметр 5.

Func<int, bool> myFunc = x => x == 5;
bool result = myFunc(4); // returns false of course

Также лямбда-выражения можно использовать, когда аргумент имеет тип Expression<Func>, например в стандартных операторах запросов, как указано в System.Linq.Queryable. При определении аргумента Expression<Func> лямбда компилируется в дерево выражений.

Ниже показан метод Count``1, являющийся стандартным оператором запроса.

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
int oddNumbers = numbers.Count(n => n % 2 == 1);

Компилятор может вывести тип входного параметра ввода; но его также можно определить явным образом. Данное лямбда-выражение подсчитывает указанные целые значения (n), которые при делении на два дают остаток 1.

Следующая строка кода создает последовательность, которая содержит все элементы массива numbers, расположенные слева от 9, поскольку это первое число последовательности, не удовлетворяющее условию:

var firstNumbersLessThan6 = numbers.TakeWhile(n => n < 6);

В этом примере показано, как определить несколько входных параметров путем их заключения в скобки. Этот метод возвращает все элементы в массиве чисел до того числа, величина которого меньше номера его позиции. Не следует путать лямбда-оператор (=>) с оператором "больше или равно" (>=).

var firstSmallNumbers = numbers.TakeWhile((n, index) => n >= index);

Вывод типа в лямбда-выражениях

При написании лямбда-выражений обычно не требуется указывать тип входных параметров, поскольку компилятор может выводить этот тип на основе тела лямбда-выражения, типа делегата параметра и других факторов, как описано в спецификации языка C#. Для большинства стандартных операторов запросов первой входное значение имеет тип элементов в исходной последовательности. Поэтому при запросе IEnumerable<Customer> входная переменная считается объектом Customer, а это означает, что у вас есть доступ к его методам и свойствам.

customers.Where(c => c.City == "London");

Общие правила для лямбда-выражений формулируются следующим образом:

  • лямбда-выражение должно содержать то же число параметров, что и тип делегата;

  • каждый входной параметр в лямбда-выражении должен быть неявно преобразуемым в соответствующий параметр делегата;

  • возвращаемое значение лямбда-выражения (если таковое имеется) должно быть неявно преобразуемым в возвращаемый тип делегата.

Обратите внимание: лямбда-выражения сами по себе не имеют типа, поскольку в системе общих типов изначально отсутствует понятие "лямбда-выражения". Однако иногда бывает удобно оперировать понятием "типа" применительно к лямбда-выражениям. При этом под типом понимается тип делегата или тип Expression, в который преобразуется лямбда-выражение.

Область действия переменной в лямбда-выражениях

Лямбда-выражения могут ссылаться на внешние переменные (см. Анонимные методы (Руководство по программированию в C#)), находящиеся в области метода, в котором определена лямбда-функция, или в области типа, который содержит лямбда-выражение. Переменные, полученные таким способом, сохраняются для использования в лямбда-выражениях, даже если бы в ином случае они оказались за границами области действия и уничтожились сборщиком мусора. Внешняя переменная должна быть определенным образом присвоена, прежде чем она сможет использоваться в лямбда-выражениях. В следующем примере демонстрируются эти правила.

delegate bool D();
delegate bool D2(int i);

class Test
{
    D del;
    D2 del2;
    public void TestMethod(int input)
    {
        int j = 0;
        // Initialize the delegates with lambda expressions.
        // Note access to 2 outer variables.
        // del will be invoked within this method.
        del = () => { j = 10;  return j > input; };

        // del2 will be invoked after TestMethod goes out of scope.
        del2 = (x) => {return x == j; };
      
        // Demonstrate value of j:
        // Output: j = 0 
        // The delegate has not been invoked yet.
        Console.WriteLine("j = {0}", j);        // Invoke the delegate.
        bool boolResult = del();

        // Output: j = 10 b = True
        Console.WriteLine("j = {0}. b = {1}", j, boolResult);
    }

    static void Main()
    {
        Test test = new Test();
        test.TestMethod(5);

        // Prove that del2 still has a copy of
        // local variable j from TestMethod.
        bool result = test.del2(10);

        // Output: True
        Console.WriteLine(result);
           
        Console.ReadKey();
    }
}

Следующие правила применимы к области действия переменной в лямбда-выражениях.

  • Захваченная переменная не будет уничтожена сборщиком мусора до тех пор, пока делегат, который на нее ссылается, не перейдет в статус подлежащего уничтожению при сборке мусора.

  • Переменные, вводимые в лямбда-выражении, невидимы во внешнем методе.

  • Лямбда-выражение не может непосредственно захватывать параметры ref или out из метода, в котором они находятся.

  • Оператор return в лямбда-выражении не вызывает возвращение значения внешним методом.

  • Лямбда-выражение не может содержать оператора goto, оператора break или оператора continue внутри лямбда-функции, если целевой объект перехода находится вне блока. Если целевой объект находится внутри блока, то наличие оператора перехода за пределами лямбда-функции также будет ошибкой.

Спецификация языка C#

Дополнительные сведения см. в Спецификация языка C#. Спецификация языка является предписывающим источником информации о синтаксисе и использовании языка C#.

Важная глава книги

Delegates, Events, and Lambda Expressions в C# 3.0 Cookbook, Third Edition: More than 250 solutions for C# 3.0 programmers

См. также

Ссылки

Анонимные методы (Руководство по программированию в C#)

is (Справочник по C#)

Основные понятия

Руководство по программированию на C#

Деревья выражений (C# и Visual Basic)

Другие ресурсы

LINQ

Visual Studio 2008 C# Samples (см. файлы LINQ Sample Queries и программу XQuery)

Рекурсивные лямбда-выражения