Лямбда выражения — это безымянные методы, написанные вместо экземпляра делегата. При компиляции лямбда выражения преобразуются либо в экземпляры делегата либо в дерево выражений, имеющее тип Expression<TDelegate>
(это позволяет перевести код выражения в объект и интерпретировать его позже — во время выполнения).
1 2 3 | delegate int Transformer (int i); Transformer sqr = x => x * x; Console.WriteLine (sqr(3)); // 9 |
В примере лямбда выражение x => x * x
записывается в качестве экземпляра делегата Transformer
. В подобной ситуации компилятор создает частный метод и перемещает код выражения в него.
Лямбда выражения имеют следующую форму:
1 | (параметры) => выражение-или-блок-инструкций |
По соглашению скобки можно опустить в том (и только в том) случае, если используется один параметр выводимого типа.
В примере выше как раз используется один параметр x
и выражение x * x
:
1 | x => x * x; |
Каждый параметр лямбда выражения соотносится с параметром делегата, а тип выражения (это может быть и void
) соотносится с типом, возвращаемым делегатом. В примере выше x
соотносится с параметром i
, а выражение x * x
соотносится с возвращаемым типом int
и поэтому совместимо с делегатом Transform
.
Вместо выражения в лямбда выражении можно использовать блок инструкций:
1 | x => { return x * x; }; |
Лямбда выражения чаще всего используются с делегатами Func
и Action
:
1 | Func<int,int> sqr = x => x * x; |
Тип параметров в лямбда выражении компилятор как правило выводит автоматически, но можно и явно указать тип параметров:
1 | Func<int,int> sqr = (int x) => x * x; |
Лямбда выражения могут принимать и несколько параметров:
1 2 3 | Func<string,string,int> totalLength = (s1, s2) => s1.Length + s2.Length; int total = totalLength ("hello", "world"); // total=10; |
Лямбда выражения могут использоваться и с событиями. Например, если предположить, Clicked
— это событие типа EventHandler
, то можно добавить обработчик с помощью лямбда выражения:
1 | obj.Clicked += (sender,args) => Console.Write ("Click"); |
Захват внешних переменных
В лямбда выражениях могут использоваться внешние переменные — локальные переменные и параметры метода, в котором эти выражения определяются.
1 2 3 4 5 6 | static void Main() { int factor = 2; Func<int, int> multiplier = n => n * factor; Console.WriteLine (multiplier (3)); // 6 } |
Внешние переменные, используемые в лямбда выражениях, называются захваченными переменными, а лямбда выражения, захватывающие переменные, называются замыканиями (closure).
Захваченные переменные вычисляются не в момент захвата, а в момент, когда вызывается делегат:
1 2 3 4 | int factor = 2; Func<int, int> multiplier = n => n * factor; factor = 10; Console.WriteLine (multiplier (3)); // 30 |
Лямбда выражения сами могут обновлять захваченные переменные:
1 2 3 4 5 | int seed = 0; Func<int> natural = () => seed++; Console.WriteLine (natural()); // 0 Console.WriteLine (natural()); // 1 Console.WriteLine (seed); // 2 |
Время жизни захваченных переменных растягивается до времени жизни делегата. В следующем примере переменная seed
при обычных обстоятельствах должна исчезнуть из области видимости после завершения выполнения метода Natural
. Но так как она является захваченной, она продолжает существовать пока существует захвативший ее делегат natural
:
1 2 3 4 5 6 7 8 9 10 11 | static Func<int> Natural() { int seed = 0; return () => seed++; // Возвращает замыкание } static void Main() { Func<int> natural = Natural(); Console.WriteLine (natural()); // 0 Console.WriteLine (natural()); // 1 } |
Захват переменных цикла
При захвате переменных цикла for, C# ведет себя с ними так, как будто они объявлены вне цикла. Поэтому на каждой интерации цикла захватывается одна и та же внешняя для цикла переменная. По этой причине в следующем примере мы получим 333, а не 012:
1 2 3 4 | Action[] actions = new Action[3]; for (int i = 0; i < 3; i++) actions [i] = () => Console.Write (i); foreach (Action a in actions) a(); // 333 |
В связи с тем, что делегатs вызываются уже после выполнения цикла for
, когда переменная i
равна 3
, а захваченные переменные, как уже отмечалось, вычисляются на момент вызова делегата, каждый делегат видит значение переменной i
на момент его вызова, а это 3
.
Если же нам нужно получить не 333
, а 012
, то единственным решением является присваивать значение переменной цикла на каждой интерации какой-либо локальной переменной, объявляемой внутри цикла, и захватывать уже эту локальную переменную:
1 2 3 4 5 6 7 | Action[] actions = new Action[3]; for (int i = 0; i < 3; i++) { int loopScopedi = i; actions [i] = () => Console.Write (loopScopedi); } foreach (Action a in actions) a(); // 012 |
Цикл foreach
в предыдущих версиях языка работал также как цикл for
, но начиная с версии C# 5.0 можно использовать переменные цикла foreach
в замыканиях непосредственно, не создавая временных локальных переменных на каждой интерации.
Анонимные методы
Анонимные метод — это разновидность лямбда выражений. Они очень похожи на лямбда выражения, за исключением нескольких моментов:
- параметры для анонимных методов не являются обязательными — их можно опускать
- синтаксис несколько отличается от лямбда выражений — анонимные методы всегда являются блоком инструкций
- анонимные методы могут быть скомпилированы в дерево выражений
Чтобы записать анонимный метод необходимо использовать ключевое слово delegate, после которого объявляются параметры (не обязательно) и в конце тело метода:
1 2 3 | delegate int Transformer (int i); Transformer sqr = delegate (int x) {return x * x;}; // анонимный метод Console.WriteLine (sqr(3)); // 9 |
Тоже самое можно записать с помощью лямбда выражения:
1 | Transformer sqr = (int x) => {return x * x;}; |
Или еще короче:
1 | Transformer sqr = x => x * x; |
У анонимных методов есть одна уникальная особенность — можно полностью опустить объявление параметров, даже если делегат их ожидает. Эта особенность может быть полезна при объявлении событий с пустым обработчиком по умолчанию:
1 | public event EventHandler Clicked = delegate { }; |
Это избавляет от необходимости проверять делегат на null перед генерацией события. Тело метода не обязательно должно быть пустым. Например, следующее тоже допустимо:
1 | Clicked += delegate { Console.Write ("clicked"); }; |
Анонимные методы могут захватывать внешние переменные точно также как лямбда выражения.