Лямбда выражения что это
Как начать работать с Лямбда-выражениями в Java
Привет, Хабр! Представляю вашему вниманию перевод статьи «How to start working with Lambda Expressions in Java» автора Luis Santiago.
До того как Лямбда-выражения были добавлены в JDK 8, я использовал их в таких языках как C# и С++. Когда они были добавлены в Java я стал изучать их подробнее.
С добавлением Лямбда-выражений добавились элементы синтаксиса, которые увеличивают «выразительную силу» Java. В этой статье я хочу сосредоточиться на основополагающих концепциях, с которыми вам необходимо познакомиться, чтобы начать использовать Лямбда-выражения.
Краткое введение
Лямбда-выражения используют преимущества параллельных процессов в многоядерных средах, что видно при поддержке операций с конвейерами данных в Stream API.
Это анонимные методы (методы без имени), используемые для реализации метода, определенного функциональным интерфейсом. Важно знать, что такое функциональный интерфейс, прежде чем вы начнете использовать Лямбда-выражения.
Функциональный интерфейс
Функциональный интерфейс — это интерфейс, содержащий один и только один абстрактный метод.
Если вы посмотрите на определение стандартного интерфейса Runnable, то вы заметите как он попадает в определение функционального интерфейса, поскольку он определяет только один метод: run().
Оператор Стрелка
В левой части задаются параметры, необходимые для выражения. Эта часть может быть пустой если не требуется никаких параметров.
Правая сторона — это тело выражения, которое определяет его действия.
Теперь, используя функциональные выражения и оператор стрелки можно составить просто лямбда-выражение:
Переменные morningGreeting и eveningGreeting в строках 6 и 7 соответственно в примере выше создают ссылку на интерфейс MyGreeting и определяют 2 выражения приветствия.
При написании лямбда-выражения можно явно указать тип параметра, как это делается в примере ниже:
Блок Лямбда-выражений
До сих пор я рассматривал одиночные лямбда-выражения. Существует еще один тип выражения, когда справа от оператора стрелки находится не одно простое выражение и так называемый блок лямбда:
Функциональные интерфейсы generic
Лямбда-выражения не могут быть generic, но функциональный интерфейс, связанный с выражением, может. Можно написать один общий интерфейс и возвращать различные типы данных, например:
Использование Лямбда-выражений в качестве аргументов
Одно распространенное использование лямбда — передача их в качестве аргументов.
Вы можете передавать исполняемый код аргументам методов в качестве параметров. Для этого просто убедитесь, что тип функционального интерфейса совместим с требуемым параметром.
Эти концепции дадут вам хорошую основу для начала работы с лямбда-выражениями. Взгляните на свой код и посмотрите, где вы можете их использовать.
Объяснение лямбда-выражений
У меня возникли вопросы о лямбда-выражениях и RxJava. Эти вопросы в основном касаются не полного понимания лямбда-выражений или RxJava. Я попытаюсь объяснить лямбда-выражения как можно проще. RxJava я опишу отдельно.
Лямбда-выражения и RxJava
Что такое лямбда-выражения? Лямбда-выражения – это «всего лишь» новый способ сделать то же самое, что мы всегда могли сделать, но в более чистом и менее многословном новом способе использования анонимных внутренних классов.
Анонимный внутренний класс в Java – это класс без имени, он должен использоваться, если вам необходимо переопределить методы класса или интерфейса. Анонимный внутренний класс может быть создан из класса или интерфейса.
В Android мы обычно используем анонимный внутренний класс в качестве слушателя, например, для кнопок такого рода:
Вернемся к лямбда-выражениям. Это следующая часть, которая является частью предыдущего кода, который считается анонимным внутренним классом.
Лямбда-выражения могут использоваться только в том случае, если вам нужно переопределить не более одного метода. К счастью для нас, View.OnClickListener содержит только один. Посмотрите на код ниже. Как думаете, какую его часть нам придётся убрать?
В некоторых случаях вам может потребоваться добавить тип к параметру, если компилятору не удается его угадать, вы можете сделать это, добавив его перед параметром:
Вы также можете использовать многострочный код:
Если у вас есть интерфейс с методом, принимающим два параметра…
…лямбда-выражение будет выглядеть следующим образом:
Если метод имеет возвращаемый тип…
…лямбда-выражение будет выглядеть следующим образом:
Но это еще не все, есть некоторые специальные случаи, которые делают код еще меньше. Если тело вашего метода содержит только одну строку кода, вы можете удалить фигурные скобки <>. Если вы удаляете фигурные скобки, вам также необходимо удалить точку с запятой, оставив следующее:
Есть еще одна вещь, которую мы можем сделать. Если у нас только одна строка кода, то компилятор может понять, нужна ли возвращаемая часть или нет, поэтому мы можем оставить ее следующим образом:
Если бы у нас был многострочный код, это свелось бы к следующему:
Так что же мы сделали? Мы взяли это:
и превратили вот в это:
Осталось только одна вещь, ссылки на методы. Допустим, у нас есть интерфейс, как и раньше, и метод, который этот интерфейс принимает как параметр:
Без лямбда-выражения это выглядело бы так:
Добавив лямбда-выражение, как мы это делали раньше, получим следующее:
Но это еще не все. Если используемый код – однострочный и вызываемая функция принимает один параметр, мы можем передавать ссылку на метод в таком виде:
Параметр будет передаваться автоматически, без необходимости использования другого кода! Удивительно, правда? Надеюсь, вы узнали что-то новое!
C++0x (С++11). Лямбда-выражения
Буквально на днях случайно наткнулся на Хабре на статью о лямбда-выражениях из нового (будущего) стандарта C++. Статья хорошая и даёт понять преимущества лямбда-выражений, однако, мне показалось, что статья недостаточно полная, поэтому я решил попробовать более детально изложить материал.
Вспомним основы
Лямбда-выражения — одна из фич функциональных языков, которую в последнее время начали добавлять также в императивные языки типа C#, C++ etc. Лямбда-выражениями называются безымянные локальные функции, которые можно создавать прямо внутри какого-либо выражения.
В прошлой статье лямбда-выражения сравнивали с указателями на функции и с функторами. Так вот первое, что следует уяснить: лямбда-выражения в C++ — это краткая форма записи анонимных функторов. Рассмотрим пример:
Фактически данный код целиком соответствует такому:
Вывод соответственно будет следующим:
На что здесь стоит обратить внимание. Во-первых, из Листинга 1 мы видим, что лямбда-выражение всегда начинается с [] (скобки могут быть непустыми — об этом позже), затем идет необязательный список параметров, а затем непосредственно тело функции. Во-вторых, тип возвращаемого значения мы не указывали, и по умолчанию лямбда возвращает void (далее мы увидим, как и зачем можно указать возвращаемый тип явно). В-третьих, как видно по Листингу 2, по умолчанию генерируется константный метод (к этому тоже еще вернемся).
Не знаю, как вам, но мне for_each, записанный с помощью лямбда-выражения, нравится гораздо больше. Попробуем написать немного усложненный пример:
В данном случае лямбда играет роль унарного предиката, то есть тип возвращаемого значения bool, хотя мы нигде этого не указывали. При наличии одного return в лямбда-выражении, компилятор вычисляет тип возвращаемого значения самостоятельно. Если же в лямбда-выражении присутствует if или switch (или другие сложные конструкции), как в приведенном ниже коде, то на компилятор полагаться уже нельзя:
Код из Листинга 4 не компилируется, а, к примеру, Visual Studio пишет ошибку на каждый return такого содержания:
Компилятор не может самостоятельно вычислить тип возвращаемого значения, поэтому мы должны его указать явно:
Теперь компиляция проходит успешно, а вывод, как и ожидалось, будет следующим:
Единственное, что мы добавили в Листинге 5, это тип возвращаемого значения для лямбда-выражения в виде -> double. Синтаксис немного странноват и смахивает больше на Haskell, чем на C++. Но указывать возвращаемый тип «слева» (как в функциях) не получилось бы, потому что лямбда должна начинаться с [], чтобы компилятор смог её различить.
Захват переменных из внешнего контекста
Все лямбда-выражения, приведенные выше, выглядели как анонимные функции, потому что не хранили никакого промежуточного состояния. Но лямбда-выражения в C++ — это анонимные функторы, а значит состояние они хранить могут! Используя лямбда-выражения, напишем программу, которая выводит количество чисел, попадающих в заданный пользователем интервал [lower; upper):
Наконец, мы добрались до того момента, когда лямбда-выражение начинается не с пустых скобок. Как видно в Листинге 6, внутри квадратных скобок могут указываться переменные. Это называется… эээм… «список захвата» (capture list). Для чего это нужно? На первый взгляд может показаться, что внешней областью видимости для лямбда-выражения является функция main() и мы можем беспрепятственно использовать переменные, объявленные в ней, внутри тела лямбда-выражения, однако это не так. Почему? Потому что фактически тело лямбды — это тело перегруженного operator()() (как бы это назвать… оператора функционального вызова что ли) внутри анонимного функтора, то есть для кода из Листинга 6 компилятор неявно сгенерирует примерно такой код:
Листинг 7 немного всё разъясняет. Наша лямбда превратилась в функтор, внутри тела которого мы не можем напрямую использовать переменные, объявленные в main(), так как это непересекающиеся области видимости. Для того чтобы доступ к lowerBound и upperBound все-таки был, эти переменные сохраняются внутри самого функтора (происходит тот самый «захват»): конструктор их инициализирует, а внутри operator()() они используются. Я специально дал этим переменным имена, начинающиеся с префикса «m_», чтобы подчеркнуть различие.
Если мы попытаемся изменить «захваченные» переменные внутри лямбды, нас ждет неудача, потому что по умолчанию генерируемый operator()() объявлен как const. Для того чтобы это обойти, мы можем указать спецификатор mutable, как в следующем примере:
Ранее я упоминал, что список параметров лямбды можно опускать, когда он пустой, однако для того чтобы компилятор правильно распарсил применение слова mutable, мы должны явно указать пустой список параметров.
При выполнении программы из Листинга 8 получаем следующее:
Как видим, благодаря ключевому слову mutable, мы можем менять значение «захваченной» переменной внутри тела лямбда-выражения, но, как и следовало ожидать, эти изменения не отражаются на локальной переменной, так как захват происходит по значению. C++ позволяет нам захватывать переменные по ссылке и даже указывать «режим захвата», используемый по умолчанию. Что это означает? Мы можем не указывать каждую переменную в списке захвата по отдельности: вместо этого можно просто указать режим по умолчанию для захвата, и тогда все переменные из внешнего контекста, которые используются внутри лямбды, будут захвачены компилятором автоматически. Для указания режима захвата по умолчанию существует специальный синтаксис: [=] или [&] для захвата по значению и по ссылке соответственно. При этом для каждой переменной можно указать свой режим захвата, однако режим по умолчанию, естественно, указывается только единожды, причем в самом начале списка захвата. Вот варианты использования:
Следует отметить, что синтаксис наподобие &out в данном случае не означает взятие адреса. Его следует читать скорее как SomeType & out, то есть это просто передача параметра по ссылке. Рассмотрим пример:
В этот раз вместо явного захвата переменной init, я указал режим захвата по умолчанию: [&]. Теперь когда компилятор встречает внутри тела лямбды переменную из внешнего контекста, он автоматически захватывает её по ссылке. Вот эквивалентный Листингу 9 код:
И соответственно вывод будет следующим:
Теперь вам главное не запутаться, что, где и когда передавать по ссылке. Фактически, если мы указываем [&] и не указываем mutable, то все равно сможем менять значение захваченной переменной и это отразится на локальной, потому что operator()() const подразумевает, что мы не можем менять, на что указывает ссылка, а это и так невозможно.
Я так понял, что выполнить захват «по константной ссылке» невозможно. Ну, может, оно нам и не надо.
А что будет, если мы напишем такое, слегка надуманное лямбда-выражение внутри метода класса:
Несмотря на все наши ожидания, код не будет скомпилирован, так как компилятор не сможет захватить m_val и m_power: эти переменные вне области видимости. Вот что говорит на это Visual Studio:
Как же быть? Чтобы получить доступ к членам класса, в capture-list нужно поместить this:
Данная программа делает именно то, чего мы ожидали:
1 2 4 8 16 32 64 128 256 512 1024
Следует заметить, что this можно захватить только по значению, и если вы попытаетесь произвести захват по ссылке, компилятор выдаст ошибку. Даже если вы в коде из Листинга 12 напишете [&] вместо [this], то this будет все равно захвачен по значению.
Прочее
Помимо всего вышеперечисленного, в заголовке лямбда-выражения можно указать throw-list — список исключений, которые лямбда может сгенерировать. Например, такая лямбда не может генерировать исключения:
А такая генерирует только bad_alloc:
Естественно, если его не указывать, то лямбда может генерировать любое исключение.
К счастью, в финальном варианте стандарта throw-спецификации объявлены устаревшими. Вместо этого оставили ключевое слово noexcept, которое говорит, что функция не должна генерировать исключение вообще.
Таким образом, общий вид лямбда-выражения следующий (сорри за такой «вольный вид» грамматики):
Повторное использование лямбда-выражений. Генерация лямбда-выражений.
Все вышеперечисленное довольно удобно, но основная мощь лямбда-выражений приходится на то, что мы можем сохранить лямбду в переменной или передавать как параметр в функцию. В Boost для этого есть класс Function, который, если я не ошибаюсь, войдет в новый стандарт STL (возможно, в немного измененном виде). На данный момент уже можно поюзать фичи из обновленного STL, однако, пока что эти фичи находятся в подпространстве имен std::tr1.
Возможность сохранения лямбда-выражений позволяет нам не только повторно использовать лямбды, но и писать функции, которые генерируют лямбда-выражения, и даже лямбды, которые генерируют лямбды.
Рассмотрим следующий пример:
Данная программа выводит:
0 1 2 3 4 5 6 7 8 9
2 3 4 5 6 7 8 9 10 11
Рассмотрим подробнее. Вначале у нас инициализируется вектор с помощью generate_n(). Тут всё просто. Далее мы создаем переменную traceLambda типа function (то есть функция, принимающая int и возвращающая void) и присваиваем ей лямбда-выражение, которое выводит на консоль значение и пробел. Далее мы используем только что сохраненную лямбду для вывода всех элементов вектора.
После этого мы видим немаленькое объявление lambdaGen, которая является лямбда-выражением, принимающим один параметр int и возвращающим другую лямбду, принимающую int и возвращающую int.
Следом за этим мы ко всем элементам вектора применяем transform(), в качестве мутационной функции для которого указываем lambdaGen(2). Фактически lambdaGen(2) возвращает другую лямбду, которая прибавляет к переданному параметру число 2 и возвращает результат. Этот код, естественно, немного надуманный, ибо то же самое можно было записать как
однако в качестве примера довольно показательно.
Затем мы снова выводим значения всех элементов вектора, используя для этого сохраненную ранее лямбду traceLambda.
На самом деле, данный код можно было записать еще короче. В новом стандарте C++ значение ключевого слова auto будет заменено. Если раньше auto означало, что переменная создается в стеке, и подразумевалось неявно в случае, если вы не указали что-либо другое (register, к примеру), то сейчас это такой себе аналог var в C# (то есть тип переменной, объявленной как auto, определяется компилятором самостоятельно на основе того, чем эта переменная инициализируется).
Следует заметить, что auto-переменная не сможет хранить значения разных типов в течение одного запуска программы. C++ как был, так и остается статически типизированным языком, и указание auto лишь говорит компилятору самостоятельно позаботиться об определении типа: после инициализации сменить тип переменной будет уже нельзя.
Кроме того что ключевое слово auto весьма полезно при работе с циклами вида
его очень удобно использовать с лямбда-выражениями. Теперь код из Листинга 13 можно переписать так:
Пожалуй, на этом я закончу описание лямбда-выражений. Если будут вопросы, поправки или замечания, с удовольствием выслушаю.
ETA (20.02.2012): Оказалось, что для кого-то эта статья до сих пор актуальна, поэтому поправил подсветку синтаксиса и подкорректировал информацию про throw-списки в объявлении лямбд. Помимо непосредственно лямбда-выражений другие фичи из нового стандарта С++11 (например, списки инициализации контейнеров) решил не добавлять, так что статья осталась практически в первозданном виде.
Привет, Дорогой читатель!
Делегаты
Делегат это особый тип. И объявляется он по особому:
Тут все просто, есть ключевое слово delegate, а дальше сам делегат с именем MyDelegate, возвращаемым типом int и одним аргументом типа string.
По факту же при компиляции кода в CIL — компилятор превращает каждый такой тип-делегат в одноименный тип-класс и все экземпляры данного типа-делегата по факту являются экземплярами соответствующих типов-классов. Каждый такой класс наследует тип MulticastDelegate от которого ему достаются методы Combine и Remove, содержит конструктор с двумя аргументами target (Object) и methodPtr (IntPtr), поле invocationList (Object), и три собственных метода Invoke, BeginInvoke, EndEnvoke.
Объявляя новый тип-делегат мы сразу через синтаксис его объявления жестко определяем сигнатуру допустимых методов, которыми могут быть инициализированы экземпляры такого делегата. Это сразу влияет на сигнатуру автогенерируемых методов Invoke, BeginInvoke, EndEnvoke, поэтому эти методы и не наследуются от базового типа а определяются для каждого типа-делегата отдельно.
Экземпляр же такого делегата стоит понимать как ссылку на конкретный метод или список методов, который куда то будет передан и скорее всего выполнен уже на той стороне. Причем клиент не сможет передать с методом значение аргументов с которыми он будет выполнен (если только мы этого ему не позволим), или поменять его сигнатуру. Но он сможет определить логику работы метода, то есть его тело.
Это удобно и безопасно для нашего кода так как мы знаем какой тип аргумента передать в делегат при выполнении и какой возвращаемый тип ожидать от делегата.
Если пофантазировать, то можно предоставить право передачи аргумента для делегатов клиентской стороне, например создать метод с аргументом делегатом и аргументом который внутри нашего метода этому делегату будет передан, что позволит клиенту задавать еще и значение аргумента для метода в делегате. Например таким образом.
Создавая в коде экземпляр делегата его конструктору передается метод (подойдет и экземплярный и статический, главное чтобы сигнатура метода совпадала с сигнатурой делегата). Если метод экземплярный то в поле target записывается ссылка на экземпляр-владелец метода (он нужен нам, ведь если метод экземплярный то это как минимум подразумевает работу с полями этого объекта target), а в methodPtr ссылка на метод. Если метод статический то записываются в поля target и methodPtr будут записаны null и ссылка на метод соответственно.
Инициализировать переменную делегата можно через создание экземпляра делегата:
Или упрощенный синтаксис без вызова конструктора:
Организовать передачу/получение экземпляра делегата можно по разному. Так как делегат это в итоге всего лишь тип-класс, то можно свободно создавать поля, свойства, аргументы методов и т.д. конкретного типа делегата.
Invoke — синхронное выполнение метода который храниться в делегате.
BeginInvoke, EndEnvoke — аналогично но асинхронное.
Вызывать выполнение методов хранящихся в делегате можно и через упрощенный синтаксис:
это аналогично записи:
А зачем делегату поле invocationList?
Поле invocationList имеет значение null для экземпляра делегата пока делегат хранит ссылку на один метод. Этот метод можно всегда перезаписать на другой приравняв через «=» переменной новый экземпляр делегата (или сразу нужного нам метода через упрощенный синтаксис). Но так же можно создать цепочку вызовов, когда делегат хранит ссылки на более чем один метод.
Для этого нужно вызвать метод Combine:
Метод Combine возвращает ссылку на новый делегат в котором поля target и methodPtr пусты, но invocationList, который содержит две ссылки на делегаты: тот что был раньше в переменной first и тот что еще хранится в second. Надо понимать что добавив третий делегат через метод Combine и записав его результат в first, то метод вернет ссылку на новый делегат с полем invocationList в котором будет коллекция из трех ссылок, а делегат с двумя ссылками будет удален сборщиком мусора при следующем цикле очистки.
При выполнении такого делегата все его методы будут выполнены по очереди. Если сигнатура делегата предполагает получение параметров то параметры будут для всех методов иметь одно значение. Если есть возвращаемое значение, то мы можем получить лишь значение последнего в списке метода.
Метод Remove же в свою очередь производит поиск в списке делегатов по значению объекта-владельца и методу, и в случае нахождения удаляет первый совпавший.
Переопределенные для делегатов операторы += и -= являются аналогами методов Combine и Remove:
аналогично следующей записи:
аналогично следующей записи:
Стоит сказать что делегаты могут быть обобщенными (Generic), что является более правильным подходом к созданию отдельных делегатов для разных типов.
Также стоит упомянуть что библиотека FCL уже содержит наиболее популярные типы делегатов (обобщенные и нет). Например делегат Action представляет собой метод без возвращаемого значения но с аргументом, а Fucn и с возвращаемым значением и аргументом.
Лямбда-операторы и лямбда-выражения
Так же экземпляр делегата можно инициализировать лямбда-оператором (lambda-operator) или лямбда-выражением (lambda-expression). Так как в целом это одно и то же, то далее по тексту я буду их просто называть «лямбды» в местах, где не нужно подчеркивать их различия.
Стоит упомянуть, что они были введены в C# 3.0, а до них существовали анонимные-функции появившиеся в C# 2.0.
Отличительной чертой лямбд является оператор =>, который делит выражение на левую часть с параметрами и правую с телом метода.
Допустим у нас есть делегат:
Тогда общий синтаксис лямбда-оператора будет следующим:
Это именно Лямбда-оператор так как мы обрамляем его тело в фигурные скобки, что позволяет нам поместить в него более одного оператора:
Допускается не указывать типы аргументов, ведь компилятор и так знает тип и сигнатуру вашего делегата, но можно и указать для простоты чтения кода другим человеком:
В случае если имеется лишь один аргумент то можно опустить обрамляющие его скобки:
Если в сигнатуре делегата аргументов нет то необходимо указать пустые скобки:
Если тело лямбды состоит лишь из одного выражения, то оно является Лямбда-выражением. Это очень удобно, так как у нас появляется возможность использовать упрощенный синтаксис в котором:
— можно опустить фигурные скобки, обрамляющие тело лямбды;
— без вышеупомянутых фигурных скобок нам не нужно использовать ключевое слово return перед оператором и точку запятой после оператора в теле лямбды:
В итоге код определения лямбды может стать крошечным: