Отличие анонимного метода от лямбды

Обновлено: 28.06.2024

На этом уроке мы рассмотрим лямбда-выражения, их типы и использование в языке C++.

Зачем нужны лямбда-выражения?

Рассмотрим следующий пример:

static bool containsNut ( std :: string_view str ) // static в данном контексте означает внутреннее связывание

Этот код перебирает массив строк в поисках первого попавшегося элемента, который содержит подстроку nut . Таким образом, результат выполнения программы:

Хотя это и рабочий код, но мы можем его улучшить.

Проблема кроется в том, что функция std::find_if() требует указатель на функцию в качестве аргумента. Из-за этого мы вынуждены определить новую функцию, которая будет использована только один раз, дать ей имя и поместить её в глобальную область видимости (т.к. функции не могут быть вложенными!). При этом она будет настолько короткой, что быстрее и проще понять её смысл, посмотрев лишь на одну строку кода, нежели изучать описание этой функции и её имя.

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

Синтаксис лямбда-выражений является одним из самых странных в языке C++, и вам может потребоваться некоторое время, чтобы к нему привыкнуть.

Лямбда-выражения имеют следующий синтаксис:

[ captureClause ] ( параметры ) -> возвращаемыйТип
стейтменты;
>

Поля captureClause и параметры могут быть пустыми, если они не требуются программисту.

Поле возвращаемыйТип является опциональным, и, если его нет, то будет использоваться вывод типа с помощью ключевого слова auto. Хотя мы ранее уже отмечали, что следует избегать использования вывода типа для возвращаемых значений функций, в данном контексте подобное использование допускается (поскольку обычно такие функции являются тривиальными).

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

Давайте перепишем предыдущий пример, но уже с использованием лямбда-выражений:

При этом всё работает точно так же, как и в случае с указателем на функцию. Результат выполнения программы аналогичен:

Обратите внимание, насколько наша лямбда похожа на функцию containsNut(). Они обе имеют одинаковые параметры и тела функций. Отметим, что у лямбды отсутствует поле captureClause (детально о captureClause мы говорим на уроке о лямбда-захватах), т.к. оно не нужно. Также для краткости мы пропустили синтаксис типа возвращаемого значения trailing, но из-за того, что operator!= возвращает значение типа bool, наша лямбда также будет возвращать логическое значение.

Тип лямбда-выражений

В примере, приведенном выше, мы определили лямбду прямо в том месте, где она была нам нужна. Такое использование лямбда-выражения иногда еще называют функциональным литералом.

Однако написание лямбды в той же строке, где она используется, иногда может затруднить чтение кода. Подобно тому, как мы можем инициализировать переменную с помощью литерала (или указателя на функцию) для использования в дальнейшем, так же мы можем инициализировать и лямбда-переменную с помощью лямбда-определения для её дальнейшего использования. Именованная лямбда вместе с удачным именем функции может облегчить чтение кода.

Например, в следующем фрагменте кода мы используем функцию std::all_of() для того, чтобы проверить, являются ли все элементы массива чётными:

Мы можем улучшить читабельность кода следующим образом:

// Хорошо: Мы можем хранить лямбду в именованной переменной и передавать её в функцию в качестве параметра

Но какого типа является лямбда в isEven ?

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

Для продвинутых читателей: На самом деле, лямбды не являются функциями (что и помогает им избегать ограничений языка C++, которые накладываются на использование вложенных функций). Лямбды являются особым типом объектов, который называется функтором. Функторы — это объекты, содержащие перегруженный operator(), который и делает их вызываемыми подобно обычным функциям.

Хотя мы не знаем тип лямбды, есть несколько способов её хранения для использования после определения. Если лямбда ничего не захватывает, то мы можем использовать обычный указатель на функцию. Как только лямбда что-либо захватывает, указатель на функцию больше не будет работать. Однако std::function может использоваться для лямбд, даже если они что-то захватывают:

std :: function addNumbers2 < // примечание: Если у вас не поддерживается C++17, используйте std::function

С помощью auto мы можем использовать фактический тип лямбды. При этом мы можем получить преимущество в виде отсутствия накладных расходов в сравнении с использованием std::function .

К сожалению, мы не всегда можем использовать auto. В тех случаях, когда фактический тип лямбды неизвестен (например, из-за того, что мы передаем лямбду в функцию в качестве параметра, и вызывающий объект сам определяет какого типа лямбда будет передана), мы не можем использовать auto. В таких случаях следует использовать std::function :

Результат выполнения программы:

Правило: Используйте auto при инициализации переменных с помощью лямбд и std::function, если вы не можете инициализировать переменную с помощью лямбд.

Общие/Обобщённые лямбды

По большей части лямбда-параметры работают так же, как и обычные параметры функций.

Одним примечательным исключением является то, что, начиная с C++14, нам разрешено использовать auto с параметрами функций.

Примечание: В C++20 обычные функции также могут использовать auto с параметрами.

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

Рассмотрим использование общей лямбды на практике:

Результат выполнения программы:

June and July start with the same letter

В примере, приведенном выше, мы использовали auto-параметры для захвата наших строк с использованием константной ссылки. Т.к. все строковые типы предоставляют доступ к своим отдельным символам через оператор [] , то нам не нужно волноваться о том, передает ли пользователь в качестве параметра std::string, строку C-style или что-то другое. Это позволяет нам написать лямбду, которая могла бы принять любой из этих объектов, то есть, если позже мы изменим тип months , — нам не придется переписывать лямбду.

Однако auto не всегда является лучшим выбором. Рассмотрим следующую программу:

Результат выполнения программы:

There are 2 months with 5 letters

В этом примере использование auto выводит тип const char* . Мы знаем, что со строками C-style трудно работать (кроме использования оператора [] ). Поэтому в данном случае для нас предпочтительнее явно определить тип параметра, как std::string_view, который позволит нам работать с базовыми типами данных намного проще (например, мы можем запросить у представления значение длины строки, даже если пользователь передал в качестве аргумента массив C-style).

Общие лямбды и статические переменные

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

Результат выполнения программы:

0: hello
1: world
0: 1
1: 2
2: ding dong

В примере, приведенном выше, мы определяем лямбду и затем вызываем её с двумя различными параметрами (строковым литералом и целочисленным типом). При этом генерируются две различные версии лямбды (одна с параметром строкового литерала, а другая — с параметром в виде целочисленного типа).

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

Мы можем видеть это в вышеприведенном примере, где каждый тип (строковые литералы и целые числа) имеет свой собственный уникальный счет! Хотя мы написали лямбду один раз, были сгенерированы две лямбды, и у каждой есть своя версия callCount .

Если бы мы хотели, чтобы callCount был общим для лямбд, то нам пришлось бы объявить его вне лямбды и захватить его по ссылке, чтобы он мог быть изменен лямбдой.

Вывод возвращаемого типа и возвращаемые типы trailing

Если использовался вывод возвращаемого типа, то возвращаемый тип лямбды выводится из стейтментов return внутри лямбды. Если использовался вывод возвращаемого типа, то все возвращаемые стейтменты внутри лямбды должны возвращать значения одного и того же типа (иначе компилятор не будет знать, какой из них ему следует использовать). Например:

auto divide < [ ] ( int x , int y , bool bInteger ) < // примечание: Не указан тип возвращаемого значения

return static_cast double > ( x ) / y ; // ОШИБКА: Тип возвращаемого значения не совпадает с предыдущим возвращаемым типом

Это приведет к ошибке компиляции, так как тип возвращаемого значения первого стейтмента return ( int ) не совпадает с типом возвращаемого значения второго стейтмента return ( double ).

В случае, когда у нас используются разные возвращаемые типы, у нас есть два варианта:

выполнить явные преобразования в один тип;

явно указать тип возвращаемого значения для лямбды и позволить компилятору выполнить неявные преобразования.

Второй вариант обычно является более предпочтительным:

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

Функциональные объекты Стандартной библиотеки С++

Для основных операций (например, сложения, вычитания или сравнения) вам не нужно писать свои собственные лямбды, потому что Стандартная библиотека С++ поставляется со многими базовыми вызываемыми объектами, которые вы можете использовать. Эти объекты определены в заголовочном файле functional. Например:

Результат выполнения программы:

99 90 80 40 13 5

Вместо преобразования функции greater() в лямбду, мы можем использовать std::greater :

std :: sort ( arr . begin ( ) , arr . end ( ) , std :: greater < >) ; // примечание: Требуются фигурные скобки для создания объекта

Результат выполнения программы:

99 90 80 40 13 5

Заключение

Лямбды великолепны, но они не заменяют обычные функции для всех случаев. Используйте обычные функции для нетривиальных случаев.

Задание №1

Создайте структуру Student , которая будет хранить имя и баллы студента. Создайте массив студентов и используйте функцию std::max_element() для поиска студента с наибольшими баллами, а затем выведите на экран имя найденного студента. Функция std::max_element() принимает begin и end списка, и функцию с двумя параметрами, которая возвращает true , если первый аргумент меньше второго.

При использовании следующего массива:

Результатом выполнения вашей программы должно быть следующее:

Dan is the best student

Показать подсказку

Ответ №1

Задание №2

Используйте std::sort() и лямбду в следующем коде для сортировки времен года по возрастанию средней температуры:

Результатом выполнения вашей программы должно быть следующее:

Winter
Spring
Fall
Summer

Ответ №2

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

Алгоритмы в Стандартной библиотеке С++

Комментариев: 16

В первом задании теста не мог понять, о какой функции max_element() идёт речь. Решил посмотреть что там в подсказке, понятнее не стало.
Погуглил и выяснил, что это алгоритм, подключаемый заголовочным файлом algoritm. В ответе он не подключён.
Использую VisualStudio. Это с моей стороны что-то не так или же нужно просто подключить заголовочный файл?

Объясните, пожалуйста, что значит в данной строке
return (str.find("nut") != std::string_view::npos); =>
!= std::string_view::npos); не пойму, что делает вторая половина этой строки

find возвращает итератор на нужную ячейку массива ( 0 или 2 или 6…)
но если не найдет нечего выдает -1 показывая что нету такого в массиве npos тоже что и -1 но пишут npos из за особенностей некоторых ide вот мы и получаем буловую логику если финд выдаст что угодно кроме как -1 то нужно вернуть true и в переменную запишется то что нашла эта функция если уравнение будет -1 = -1 то из за оператора не равно (!=) бул вернет false и в переменную не будет записана инфа и по итерации пойдет дальше

Дмитрий Бушуев :

std::string_view::npos — это специальная константа. Точное значение зависит от контекста, но обычно она используется как индикатор достижения конца строки в процессе некоторого поиска.

Функция str.find("nut") возвращает либо индекс первого элемента подстроки (если она найдена), либо — npos (если такой подстроки не нашлось) 🙂

Почему в первом задании теста uniform-инициализация происходит так:

?
Попробовал в VS второй вариант, и действительно, студия ругается. Но почему, в таком случае, можно спокойно объявить с помощью uniform-инициализации массив std::array целых чисел следующим образом:

И студия не будет ругаться? Почему при объявлении структур таким образом она ругается (говорит, что слишком много значений инициализатора (компилятор)). Нелогично, на мой взгляд.

Потому что ты невнимательный. В твоем примере массив из фундаментальных типов int, и он(int) принимает 1 параметр.
А в случае того же массива из структур Student, и он(Student) принимает 2 параметра.
Будь внимательней.

Вопрос про второй комплект угловых скобок. Причем здесь второй параметр?

На сколько я понял, они здесь вместо знака равно, тоесть обычное присваивание.

Вот мои наблюдения:

"Правило: Используйте auto при инициализации переменных с помощью лямбд и std::function, если вы не можете инициализировать переменную с помощью лямбд."

А мне кажется, что лучше всегда использовать std::function, тем более что после C++17 и тип возврата, и типы параметров указывать необязательно. На мой взгляд ключевое слово auto уже немного устарел 🙂

Здравствуйте) Если кому то не сложно, не могли бы вы обьяснить предложение "Функция std::max_element() принимает begin и end списка и функцию с двумя параметрами, которая возвращает true, если первый аргумент меньше второго.". Я не совсем понимаю зачем в этой функции нужен третий параметр в виде лямбды. Заранее благодарю всех кто ответит.

Дмитрий Бушуев :

Так а как вы собираетесь сравнивать двух студентов между собой? Это же составной тип данных, С++ ничего не знает про сравнение подобных переменных. Поэтому вы сами должны написать эту функцию (в виде лямбды) 🙂

Я прочитала этот урок дважды с перерывом в неделю. И всё равно есть чувство, что я поняла не всё.
Во-первых, урок очень длинный.
Во-вторых, в примерах используется std::string_view, о котором до конца апреля в этих уроках ничего не было. Также используются алгоритмы STL, которые здесь также объяснялись скорее для расширения кругозора.
В-третьих, кусок "Если лямбда ничего не захватывает, мы можем использовать обычный указатель на функцию. Как только лямбда захватывает что-либо, указатель на функцию больше не будет работать. " не ясно, что значит, что лямбда что-то захватывает. Я подозреваю, что это связано с [Capture clause], который здесь никак не поясняется, но должен быть раскрыт в следующем уроке.
В-четвёртых, главное правило данного урока "Используйте auto при инициализации переменных с помощью лямбд и std::function, если вы не можете инициализировать переменную с помощью лямбд." тоже не до конца раскрыто. Я прочитала пример, но до конца так и не поняла, в чём разница, и как это на практике разделять. Возможно, причина в том, что слово "лямбда" в данном уроке заменяет несколько разных смыслов, связанных с лямбда-выражениями. Скорей всего, речь идёт о том, что когда мы даём имя нашему лямбда-выражению, мы можем указать только тип auto. Или другие, более конкретные типы тоже возможны? Судя по тому, что я прочитала, нет, но ведь auto превращается в конкретный тип из return…
В-пятых, абзац "Если использовался вывод возвращаемого типа, то возвращаемый тип лямбды выводится из стейтментов return внутри лямбды. Если использовался вывод возвращаемого типа, то все возвращаемые стейтменты внутри лямбды должны возвращать значения одного и того же типа…" Два предложения, начинающихся с одного и того же условия "Если использовался вывод возвращаемого типа" почему бы тогда не переписать это как-то так, чтобы не путать читателя:
Если использовался вывод возвращаемого типа:
1) возвращаемый тип лямбды выводится из стейтментов return внутри лямбды
2) все возвращаемые стейтменты внутри лямбды должны возвращать значения одного и того же типа
Хотя нет, даже так не понятно, что имеется в виду под "вывод" — печать на экране? В лямбда-выражении или в вызывающей функции?
В общем, здесь я попыталась проанализировать, что же заставило меня прокрастинировать с этим уроком целую неделю и почему до сих пор даже после второго прочтения у меня нет чувства, что "всё понятно". Хотя, судя по всему, тема несложная.

Юрий :

1. Урок действительно больше обычного урока по С++, но посмотрите на уроки по OpenGL и тогда этот урок Вам не будет казаться таким уж большим 🙂

2. Вместе с этим уроком были добавлены уроки по std::string_view и по алгоритмам.

3. Есть отдельный урок и по лямбда-захватам (они же capture clause).

Я понимаю, что анонимные методы могут использоваться для определения делегатов и написания встроенных функций. Отличается ли использование лямбда-выражений от этого?

Я думаю, я немного запутался, когда что использовать.

Лямбда-выражение - это просто сокращенный синтаксис для анонимного метода. Анонимные методы выглядят так:

Эквивалентное лямбда-выражение будет выглядеть так:

Короче говоря, все лямбда-выражения являются анонимными методами, но возможно иметь анонимный метод, который не написан в лямбда-синтаксисе (как в первом примере выше). Надеюсь, что это полезно!

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

Только лямбда-выражение без тела метода может быть преобразовано в дерево выражений

Следующие конструкции компилируются:

И следующие не

Так что есть разница даже на не очень продвинутом уровне (что Джон Скит указывает на пример больной разницы)

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

Следующие две строки демонстрируют разницу

Вы делаете по сути то же самое, но анонимный делегат здесь явно выглядит лучше.

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

Лямбда выражения, имеющая выражение в качестве текста:

Лямбда оператора, имеющая блок операторов в качестве текста:

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

Лямбда-выражение может быть преобразовано в тип делегата. Тип делегата, в который может быть преобразовано лямбда-выражение, определяется типами его параметров и возвращаемым значением. Если лямбда-выражение не возвращает значение, оно может быть преобразовано в один из типов делегата Action ; в противном случае его можно преобразовать в один из типов делегатов Func . Например, лямбда-выражение, которое имеет два параметра и не возвращает значение, можно преобразовать в делегат Action . Лямбда-выражение, которое имеет два параметра и возвращает значение, можно преобразовать в делегат Func . В следующем примере лямбда-выражение x => x * x , которое указывает параметр с именем x и возвращает значение x в квадрате, присваивается переменной типа делегата:

Лямбда-выражения можно также преобразовать в типы дерева выражения, как показано в следующем примере:

При использовании синтаксиса на основе методов для вызова метода Enumerable.Select в классе System.Linq.Enumerable (например, в LINQ to Objects и LINQ to XML) параметром является тип делегата System.Func . При вызове метода Queryable.Select в классе System.Linq.Queryable (например, в LINQ to SQL) типом параметра является тип дерева выражения Expression > . В обоих случаях можно использовать одно и то же лямбда-выражение для указания значения параметра. Поэтому оба вызова Select выглядят одинаково, хотя на самом деле объект, созданный из лямбда-выражения, имеет другой тип.

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

Лямбда-выражение с выражением с правой стороны оператора => называется выражением лямбда. Выражения-лямбды возвращают результат выражения и принимают следующую основную форму.

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

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

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

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

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

Два и более входных параметра разделяются запятыми:

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

Для входных параметров все типы нужно задать либо в явном, либо в неявном виде. В противном случае компилятор выдает ошибку CS0748.

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

Если только один входной параметр имеет имя _ , для обеспечения обратной совместимости _ рассматривается как имя этого параметра в лямбда-выражении.

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

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

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

Дополнительные сведения о создании и использовании асинхронных методов см. в разделе Асинхронное программирование с использованием ключевых слов Async и Await.

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

Кортеж определяется путем заключения в скобки списка его компонентов с разделителями-запятыми. В следующем примере кортеж с тремя компонентами используется для передачи последовательности чисел в лямбда-выражение. Оно удваивает каждое значение и возвращает кортеж с тремя компонентами, содержащий результат операций умножения.

Как правило, поля кортежи именуются как Item1 , Item2 и т. д. Тем не менее кортеж с именованными компонентами можно определить, как показано в следующем примере:

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

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

В этом примере используется стандартный оператор запроса Count:

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

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

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

Лямбда-выражения не используются непосредственно в выражениях запросов, но их можно использовать в вызовах методов в выражениях запросов, как показано в следующем примере:

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

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

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

Естественный тип для лямбда-выражений

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

Компилятор может определить parse как Func . В общем случае компилятор будет использовать доступный делегат Func или Action , если он существует. В противном случае он синтезирует типа делегата. Например, тип должен быть синтезирован, если лямбда-выражение имеет параметры ref . Если лямбда-выражение имеет естественный тип, его можно присвоить менее явному типу, например System.Object, или System.Delegate:

Группы методов (то есть имена методов без списков аргументов) с ровно одной перегрузкой имеют естественный тип:

У многих лямбда-выражений нет естественного типа. Рассмотрим следующее объявление:

Компилятор не может определить тип параметра для s . Если компилятор не может определить естественный тип, необходимо объявить тип:

Объявленный тип возвращаемого значения

Как правило, тип возвращаемого значения лямбда-выражения является очевидным и легко выводится. Для некоторых выражений это не сработает:

Атрибуты

Можно применить любой атрибут, допустимый для AttributeTargets.Method.

Лямбда-выражения вызываются через базовый тип делегата. Это отличается от методов и локальных функций. Метод делегата Invoke не проверяет атрибуты в лямбда-выражении. При вызове лямбда-выражения атрибуты не оказывают никакого влияния. Атрибуты лямбда-выражений полезны для анализа кода и могут быть обнаружены с помощью отражения. Одним из последствий этого решения является тот факт, что System.Diagnostics.ConditionalAttribute невозможно применить к лямбда-выражению.

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

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

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

  • Захваченная переменная не будет уничтожена сборщиком мусора до тех пор, пока делегат, который на нее ссылается, не перейдет в статус подлежащего уничтожению при сборке мусора.
  • Переменные, представленные в лямбда-выражении, невидимы в заключающем методе.
  • Лямбда-выражение не может непосредственно захватывать параметры in, ref или out из заключающего метода.
  • Оператор return в лямбда-выражении не вызывает возврат значения заключающим методом.
  • Лямбда-выражение не может содержать операторы goto, break или continue, если целевой объект этого оператора перехода находится за пределами блока лямбда-выражения. Если целевой объект находится внутри блока, использование оператора перехода за пределами лямбда-выражения также будет ошибкой.

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

Может кто-нибудь объяснить? Я понимаю основные концепции, стоящие за ними, но часто вижу, что они используются взаимозаменяемо, и я запутываюсь.

И теперь, когда мы здесь, чем они отличаются от обычной функции?

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

Ответ SasQ превосходен. ИМХО, этот вопрос был бы более полезным для пользователей SO, если бы он направлял зрителей к этому ответу.

Лямбда просто анонимная функция - функция , определенная без имени. В некоторых языках, таких как Scheme, они эквивалентны именованным функциям. Фактически, определение функции переписывается как внутренняя привязка лямбды к переменной. В других языках, таких как Python, между ними есть некоторые (довольно ненужные) различия, но в остальном они ведут себя так же.

Замыкание любая функция , которая закрывает над на окружающую среду , в которой она была определена. Это означает, что он может обращаться к переменным, отсутствующим в его списке параметров. Примеры:

Это приведет к ошибке, потому func что не закрывается в среде anotherfunc - h не определено. func только закрывается над глобальной средой. Это будет работать:

Поскольку здесь func определено в anotherfunc и в Python 2.3 и более поздних версиях (или некотором подобном числе), когда они почти получили правильные замыкания (мутация по-прежнему не работает), это означает, что оно закрывается над anotherfunc средой и может обращаться к переменным внутри Это. В Python 3.1+, мутация тоже работает при использовании на nonlocal ключевое слово .

Еще один важный момент - func будет продолжать закрывать над anotherfunc средой, даже если она больше не оценивается в anotherfunc . Этот код также будет работать:

Это напечатает 10.

Это, как вы заметили, не имеет ничего общего с лямбдами - это два разных (хотя и связанных) понятия.

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

@AlexanderOrlov: Это и лямбды, и затворы. В Java раньше были замыкания через анонимные внутренние классы. Теперь эта функциональность стала проще синтаксически с помощью лямбда-выражений. Так что, вероятно, наиболее важным аспектом новой функции является то, что теперь есть лямбды. Это не правильно называть их лямбдами, они действительно лямбды. Почему авторы Java 8 могут не выделять тот факт, что они являются замыканиями, я не знаю.

@Claudiu Я думаю, что ссылка на конкретную языковую реализацию (Python) может усложнить ответ. Вопрос полностью не зависит от языка (а также не имеет языковых тегов).

Существует много путаницы вокруг лямбд и закрытий, даже в ответах на этот вопрос StackOverflow здесь. Вместо того, чтобы расспрашивать случайных программистов, которые узнали о замыканиях на практике с определенными языками программирования или другими невежественными программистами, отправляйтесь в путь к источнику (где все это началось). А так как лямбды и закрытие происходят из Lambda Исчисление изобретен Алонзо Черч еще в 30 - е годы , прежде чем первые электронные вычислительные машины существовали даже, это источник я говорю.

Лямбда-исчисление является самым простым языком программирования в мире. Единственное, что в нем можно сделать: ►

Итак, мы решили одну из загадок:
лямбда - это анонимная функция из приведенного выше примера λx.x+2 .

В разных языках программирования синтаксис функциональной абстракции (лямбда) может отличаться. Например, в JavaScript это выглядит так:

и вы можете сразу применить его к какому-либо параметру:

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

который фактически дает ему имя f , позволяя вам ссылаться на него и вызывать его несколько раз позже, например:

Но вы не должны были назвать это. Вы можете позвонить сразу:

В LISP лямбды сделаны так:

и вы можете вызвать такую ​​лямбду, применив ее немедленно к параметру:

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

Как я уже сказал, лямбда-абстракция выполняет привязку символа в его подвыражении, чтобы он стал заменяемым параметром . Такой символ называется связанным . Но что, если в выражении есть другие символы? Например: λx.x/y+2 . В этом выражении символ x связан лямбда-абстракцией, λx. предшествующей ему. Но другой символ y не связан - он свободен . Мы не знаем, что это и откуда это происходит, поэтому мы не знаем, что это значит и какую ценность оно представляет, и поэтому мы не можем оценить это выражение, пока не выясним, что это y значит.

На самом деле, то же самое относится и к двум другим символам, 2 и + . Просто мы настолько знакомы с этими двумя символами, что обычно забываем, что компьютер их не знает, и нам нужно сказать, что они означают, определив их где-то, например, в библиотеке или в самом языке.

Это позволяет нам разделить лямбда-выражения на две категории:

  • ЗАКРЫТЫЕ выражения: каждый символ, встречающийся в этих выражениях, ограничен какой-то лямбда-абстракцией. Другими словами, они автономны ; они не требуют какого-либо окружающего контекста для оценки. Их также называют комбинаторами .
  • Выражения OPEN: некоторые символы в этих выражениях не являются связанными, то есть некоторые символы, встречающиеся в них, являются свободными и требуют некоторой внешней информации, и поэтому они не могут быть оценены, пока вы не предоставите определения этих символов.

Вы можете ЗАКРЫТЬ открытое лямбда-выражение, предоставив среду , которая определяет все эти свободные символы, связывая их с некоторыми значениями (которые могут быть числами, строками, анонимными функциями, т. Е. Лямбдами, что угодно…).

Например, если у вас есть следующее лямбда-выражение: λx.x/y+2 символ x связан, а символ y свободен, поэтому выражение можно open и не может быть оценено, если вы не скажете, что y означает (и то же самое с + и 2 , которые также свободны). Но предположим, что у вас также есть такая среда :

и это как раз закрытие нашего лямбда-выражения:>

Другими словами, он закрывает открытое лямбда-выражение. Вот откуда и произошло закрытие имени, и поэтому ответы многих людей в этой теме не совсем верны: P

Так почему они ошибаются? Почему многие из них говорят, что замыкания - это некоторые структуры данных в памяти или некоторые особенности языков, которые они используют, или почему они путают замыкания с лямбдами? :П

И это еще хуже из-за того факта, что в их словах всегда есть доля правды, которая не позволяет вам легко отклонить это как ложное: P Позвольте мне объяснить:

Людям не потребовалось слишком много времени, чтобы начать называть фактическую структуру данных, которую они используют в реализациях своего языка, чтобы реализовать замыкание как само замыкание. Структура обычно выглядит примерно так:

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

Как я объяснил выше, закрытие лямбда-выражения - это подмножество определений в его среде, которые присваивают значения свободным переменным, содержащимся в этом лямбда-выражении, эффективно закрывая выражение (превращая открытое лямбда-выражение, которое еще не может быть оценено, в закрытая лямбда - выражение, которое затем может быть оценено, так как все символы , содержащиеся в нем теперь определены).

Я надеюсь, что это отвечает на ваши вопросы. Но если у вас есть дополнительные вопросы, не стесняйтесь задавать их в комментариях, и я постараюсь объяснить их лучше.


Поддержка лямбда-выражений, реализованная в Java 8, стала одним из наиболее значимых нововведений за последнее время. Будучи упрощённой записью анонимных классов, лямбды позволяют писать более лаконичный код при работе со Stream или Optional. Лямбда-выражения часто используются как совместно со многими API стандартной библиотеки Java, так и со сторонними API, среди которых JavaFX, реактивные стримы и т.д.

Лямбды и функциональные интерфейсы

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

Функциональный интерфейс в Java — интерфейс, в котором объявлен только один абстрактный метод. Однако, методов по умолчанию (default) такой интерфейс может содержать сколько угодно, что можно видеть на примере java.util.function.Function. Функциональный интерфейс может быть отмечен аннотацией @FunctionalInterface, но это не обязательное условие, так как JVM считает функциональным любой интерфейс с одним абстрактным методом.

Пример простого функционального интерфейса:

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

Типы аргументов лямбда-выражения опциональны, так как они декларируются интерфейсом, но при использовании обобщений (дженериков) с extends/super может возникнуть необходимость в указании конкретных типов аргументов. При этом стоит отметить, что типы либо указываются для всех аргументов, либо не указываются вообще. Это же касается и использования var, введённой в Java 11. Всё это можно свести к такому правилу: все аргументы объявляются либо с типами, либо с var, либо без них.

Если у лямбда-выражения всего один аргумент, и для него не требуется объявление типа или var, то круглые скобки можно опустить. В остальных случаях, в том числе если лямбда не принимает никаких аргументов, скобки нельзя опустить.

Аналогичная ситуация и с телом лямбда-выражений: если оно состоит только из одной строки, то фигурные скобки, точку с запятой (;) и директиву return можно тоже опустить.

В качестве тела лямбда-выражения может использоваться ссылка на метод.

Создание лямбда-выражений

Допустим, нам нужна реализация CarFilter, описанного выше, которая проверяла бы, что автомобиль выпущен не раньше 2010 года. Если мы будем использовать анонимный класс, то создание объекта CarFilter будет выглядеть примерно следующим образом:

Но мы можем описать объект CarFilter при помощи лямбда-выражения:

Однако, эту запись можно сделать ещё меньше:

Согласитесь, что такая запись зачительно меньше и лаконичнее, чем использование анонимного класса.

Применение лямбда-выражений

Допустим у нас есть задача написать метод, выводящий из полученного списка автомобили, у которых тип кузова (body) — STATION_WAGON и мощность (power) — больше 200 л.с.

Скорее всего, мы напишем что-то вроде:

В целом, если нам требуется всего один подобный метод, то этот код можно оставить без изменений и даже не задумываться об использовании лямбда-выражений. Но, допустим, у нас появляется задача реализовать ещё один метод, который бы выводил все автомобили, у которых кузов не PICKUP_TRUCK, или метод, который бы сохранял в БД все автомобили с мощностью двигателя более 150 л.с.

В этом случае логично было бы использовать сразу два функциональных интерфейса: java.util.function.Predicate — для фильтрации и java.util.function.Consumer — для действия, применяемого к подходящим объектам.

java.util.function.Predicate декларирует абстрактный метод test, который принимает объект и возвращает значение типа boolean в зависимости от соответствия переданного объекта требуемым критериям.

java.util.function.Consumer декларирует абстрактный метод accept, который принимает объект и выполняет над ним требуемые действия.

Метод printCars превратится во что-то похожее на следующий метод:

И первоначальную задачу вывести из полученного списка автомобили, у которых тип кузова (body) — STATION_WAGON и мощность (power) — больше 200 л.с. мы решили бы следующим вызовом метода processCars с использованием лямбда-выражений:

Или при помощи анонимных классов:

Вариант вызова метода processCars с использованием лямбда-выражений значительно компактнее.

Лямбды, анонимные классы и обычные классы

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

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

Но в большинстве случаев, там где можно применять лямбда-выражения, например в Stream, Optional или CompletableFuture, логичнее применять именно лямбды.

Полезные ссылки

Читайте также: