THE BELL

Есть те, кто прочитали эту новость раньше вас.
Подпишитесь, чтобы получать статьи свежими.
Email
Имя
Фамилия
Как вы хотите читать The Bell
Без спама

Среди новшеств, которые были привнесены в язык Java с выходом JDK 8, особняком стоят лямбда-выражения. Лямбда представляет набор инструкций, которые можно выделить в отдельную переменную и затем многократно вызвать в различных местах программы.

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

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

Рассмотрим пример:

Public class LambdaApp { public static void main(String args) { Operationable operation; operation = (x,y)->x+y; int result = operation.calculate(10, 20); System.out.println(result); //30 } } interface Operationable{ int calculate(int x, int y); }

В роли функционального интерфейса выступает интерфейс Operationable , в котором определен один метод без реализации - метод calculate . Данный метод принимает два параметра - целых числа, и возвращает некоторое целое число.

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

Public class LambdaApp { public static void main(String args) { Operationable op = new Operationable(){ public int calculate(int x, int y){ return x + y; } }; int z = op.calculate(20, 10); System.out.println(z); // 30 } } interface Operationable{ int calculate(int x, int y); }

Чтобы объявить и использовать лямбда-выражение, основная программа разбивается на ряд этапов:

    Operationable operation;

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

    Operation = (x,y)->x+y;

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

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

    Использование лямбда-выражения в виде вызова метода интерфейса:

    Int result = operation.calculate(10, 20);

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

При этом для одного функционального интерфейса мы можем определить множество лямбда-выражений. Например:

Operationable operation1 = (int x, int y)-> x + y; Operationable operation2 = (int x, int y)-> x - y; Operationable operation3 = (int x, int y)-> x * y; System.out.println(operation1.calculate(20, 10)); //30 System.out.println(operation2.calculate(20, 10)); //10 System.out.println(operation3.calculate(20, 10)); //200

Отложенное выполнение

Одним из ключевых моментов в использовании лямбд является отложенное выполнение (deferred execution). То есть мы определяем в одном месте программы лямбда-выражение и затем можем его вызывать при необходимости неопределенное количество раз в различных частях программы. Отложенное выполнение может потребоваться, к примеру, в следующих случаях:

    Выполнение кода отдельном потоке

    Выполнение одного и того же кода несколько раз

    Выполнение кода в результате какого-то события

    Выполнение кода только в том случае, когда он действительно необходим и если он необходим

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

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

Operation = (int x, int y)->x+y;

Если метод не принимает никаких параметров, то пишутся пустые скобки, например:

()-> 30 + 20;

Если метод принимает только один параметр, то скобки можно опустить:

N-> n * n;

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

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

Interface Printable{ void print(String s); } public class LambdaApp { public static void main(String args) { Printable printer = s->System.out.println(s); printer.print("Hello Java!"); } }

Лямбды и локальные переменные

Лямбда-выражение может использовать переменные, которые объявлены во вне в более общей области видимости - на уровне класса или метода, в котором лямбда-выражение определено. Однако в зависимости от того, как и где определены переменные, могут различаться способы их использования в лямбдах. Рассмотрим первый пример - использования переменных уровня класса:

Public class LambdaApp { static int x = 10; static int y = 20; public static void main(String args) { Operation op = ()->{ x=30; return x+y; }; System.out.println(op.calculate()); // 50 System.out.println(x); // 30 - значение x изменилось } } interface Operation{ int calculate(); }

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

Теперь рассмотрим другой пример - локальные переменные на уровне метода:

Public static void main(String args) { int n=70; int m=30; Operation op = ()->{ //n=100; - так нельзя сделать return m+n; }; // n=100; - так тоже нельзя System.out.println(op.calculate()); // 100 }

Локальные переменные уровня метода мы также может использовать в лямбдах, но изменять их значение мы уже не сможем. Если мы попробуем это сделать, то среда разработки (Netbeans) может нам высветить ошибку и то, что такую переменную надо пометить с помощью ключевого слова final , то есть сделать константой: final int n=70; . Однако это необязательно.

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

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

Существуют два типа лямбда-выражений: однострочное выражение и блок кода. Примеры однострочных выражений демонстрировались выше. Блочные выражения обрамляются фигурными скобками. В блочных лямбда-выражениях можно использовать внутренние вложенные блоки, циклы, конструкции if, switch, создавать переменные и т.д. Если блочное лямбда-выражение должно возвращать значение, то явным образом применяется оператор return:

Operationable operation = (int x, int y)-> { if(y==0) return 0; else return x/y; }; System.out.println(operation.calculate(20, 10)); //2 System.out.println(operation.calculate(20, 0)); //0

Обобщенный функциональный интерфейс

Функциональный интерфейс может быть обобщенным, однако в лямбда-выражении использование обобщений не допускается. В этом случае нам надо типизировать объект интерфейса определенным типом, который потом будет применяться в лямбда-выражении. Например:

Public class LambdaApp { public static void main(String args) { Operationable operation1 = (x, y)-> x + y; Operationable operation2 = (x, y) -> x + y; System.out.println(operation1.calculate(20, 10)); //30 System.out.println(operation2.calculate("20", "10")); //2010 } } interface Operationable{ T calculate(T x, T y); }

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

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

Истоки

В далёких 1930-х молодой американский математик Алонзо Чёрч задался вопросом о том, что значит «вычислить» что-либо. Плодом его размышлений явилась система для формализации понятия «вычисление», и назвал он эту систему «лямбда-исчислением» (англ. lambda calculus, по имени греческой буквы λ). В основе этой системы лежит лямбда-функция, которую в некотором смысле можно считать «матерью функционального программирования» в целом и Haskell в частности. Далее буду называть её ЛФ.

В отношении ЛФ можно смело сказать: «Всё гениальное просто». Идея ЛФ столь полезна именно потому, что она предельно проста. ЛФ - это анонимная функция. Вот как она выглядит в Haskell:

\x -> x * x

Обратный слэш в начале - признак ЛФ. Сравните с математической формой записи:

λx . x * x

Похоже, не правда ли? Воспринимайте обратный слэш в определении ЛФ как спинку буквы λ .

ЛФ представляет собой простейший вид функции, эдакая функция, раздетая догола. У неё забрали не только объявление, но и имя, оставив лишь необходимый минимум в виде имён аргументов и внутреннего выражения. Алонзо Чёрч понял: чтобы применить функцию, вовсе необязательно её именовать. И если у обычной функции сначала идёт объявление/определение, а затем (где-то) применение с использованием имени, то у ЛФ всё куда проще: мы её определяем и тут же применяем, на месте. Вот так:

(\x -> x * x) 5

Помните функцию square ? Вот это её лямбда-аналог:

(\x -> x * x) 5 лямбда- абстракция аргумент

Лямбда-абстракция (англ. lambda abstraction) - это особое выражение, порождающее функцию, которую мы сразу же применяем к аргументу 5 . ЛФ с одним аргументом, как и простую функцию, называют ещё «ЛФ от одного аргумента» или «ЛФ одного аргумента». Также можно сказать и о «лямбда-абстракции от одного аргумента».

Строение

Строение лямбда-абстракции предельно простое:

\ x -> x * x признак имя выражение ЛФ аргумента

Соответственно, если ЛФ применяется к двум аргументам - пишем так:

\ x y -> x * y признак имя 1 имя 2 выражение ЛФ аргумента аргумента

И когда мы применяем такую функцию:

(\x y -> x * y) 10 4

то просто подставляем 10 на место x , а 4 - на место y , и получаем выражение 10 * 4:

(\x y -> x * y) 10 4 = 10 * 4 = 40

В общем, всё как с обычной функцией, даже проще.

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

Теперь мы можем применять mul так же, как если бы это была сама лямбда-абстракция:

mul 10 4 = (\x y -> x * y) 10 4 = 10 * 4

И здесь мы приблизились к одному важному открытию.

Тип функции

Мы знаем, что у всех данных в Haskell-программе обязательно есть какой-то тип, внимательно проверяемый на этапе компиляции. Вопрос: какой тип у выражения mul из предыдущего примера?

Ответ прост: тип mul такой же, как и у этой лямбда-абстракции. Из этого мы делаем важный вывод: ЛФ имеет тип, как и обычные данные. Но поскольку ЛФ является частным случаем функции - значит и у обыкновенной функции тоже есть тип!

В нефункциональных языках между функциями и данными проведена чёткая граница: вот это функции, а вон то - данные. Однако в Haskell между данными и функциями разницы нет, ведь и то и другое покоится на одной и той же Черепахе. Вот тип функции mul:

mul:: a -> a -> a

Погодите, скажете вы, но ведь это же объявление функции! Совершенно верно: объявление функции - это и есть указание её типа. Помните, когда мы впервые познакомились с функцией, я уточнил, что её объявление разделено двойным двоеточием? Так вот это двойное двоеточие и представляет собой указание типа:

mul:: a -> a -> a вот имеет │ вот │ это тип └─ такой ─┘

Точно так же мы можем указать тип любых других данных:

let coeff = 12 :: Double

Хотя мы знаем, что в Haskell типы выводятся автоматически, иногда мы хотим взять эту заботу на себя. В данном случае мы явно говорим: «Пусть выражение coeff будет равно 12 , но тип его пусть будет Double , а не Int ». Так же и с функцией: когда мы объявляем её - мы тем самым указываем её тип.

Но вы спросите, можем ли мы не указывать тип функции явно? Можем:

square x = x * x

Это наша старая знакомая, функция square . Когда она будет применена к значению типа Int , тип аргумента будет выведен автоматически как Int .

И раз функция характеризуется типом так же, как и прочие данные, мы делаем ещё одно важное открытие: функциями можно оперировать как данными. Например, можно создать список функций:

Выражение functions - это список из двух функций. Два лямбда-выражения порождают эти две функции, но до момента применения они ничего не делают, они безжизненны и бесполезны. Но когда мы применяем функцию head к этому списку, мы получаем первый элемент списка, то есть первую функцию. И получив, тут же применяем эту функцию к строке "Hi" :

putStrLn ((head functions) "Hi" ) │ первая │ её │ функция │ аргумент └─ из списка ──┘

Это равносильно коду:

putStrLn ((\x -> x ++ " val1" ) "Hi" )

При запуске программы мы получим:

Hi val1

Кстати, а каков тип списка functions ? Его тип таков: . То есть список функций с одним аргументом типа String , возвращающих значение типа String .

Локальные функции

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

Здесь определены функции -- isInfixOf и isSuffixOf. import Data.List validComEmail:: String -> Bool validComEmail email = containsAtSign email && endsWithCom email where containsAtSign e = "@" `isInfixOf` e endsWithCom e = ".com" `isSuffixOf` e main:: IO () main = putStrLn (if validComEmail my then "It"s ok!" else "Non-com email!" ) where my = " "

Несколько наивная функция validComEmail проверяет.com -адрес. Её выражение образовано оператором && и двумя выражениями типа Bool . Вот как образованы эти выражения:

containsAtSign e = "@" `isInfixOf` e endsWithCom e = ".com" `isSuffixOf` e

Это - две функции, которые мы определили прямо в where -секции, поэтому они существуют только для основного выражения функции validComEmail . С простыми функциями так поступают очень часто: где она нужна, там её и определяют. Мы могли бы написать и более явно:

validComEmail:: String -> Bool validComEmail email = containsAtSign email && endsWithCom email where -- Объявляем локальную функцию явно. containsAtSign:: String -> Bool containsAtSign e = "@" `isInfixOf` e -- И эту тоже. endsWithCom:: String -> Bool endsWithCom e = ".com" `isSuffixOf` e

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

Вот как этот код выглядит с лямбда-абстракциями:

Теперь выражения containsAtSign и endsWithCom приравнены к ЛФ от одного аргумента. В этом случае мы не указываем тип этих выражений. Впрочем, если очень хочется, можно и указать:

containsAtSign = (\e -> "@" `isInfixOf` e) :: String -> Bool лямбда- абстракция тип этой абстракции

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

containsAtSign = \e -> "@" `isInfixOf` e:: String -> Bool в этом случае это тип аргумента e, а вовсе не всей функции!

Для типа функции тоже можно ввести псевдоним:

Псевдоним для типа функции. type Func = String -> Bool validComEmail:: String -> Bool validComEmail email = containsAtSign email && endsWithCom email where containsAtSign = (\e -> "@" `isInfixOf` e) :: Func endsWithCom = (\e -> ".com" `isSuffixOf` e) :: Func

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

Отныне, познакомившись с ЛФ, мы будем использовать их периодически.

И напоследок, вопрос. Помните тип функции mul ?

mul:: a -> a -> a

Что это за буква a ? Во-первых, мы не встречали такой тип ранее, а во-вторых, разве имя типа в Haskell не обязано начинаться с большой буквы? Обязано. А всё дело в том, что буква a в данном случае - это не совсем имя типа. А вот что это такое, мы узнаем в одной из ближайших глав.

Для любопытных

А почему, собственно, лямбда? Почему Чёрч выбрал именно эту греческую букву? По одной из версий, произошло это чисто случайно.

Шли 30-е годы прошлого века, компьютеров не было, и все научные работы набирались на печатных машинках. В первоначальном варианте, дабы выделять имя аргумента ЛФ, Чёрч ставил над именем аргумента символ, похожий на ^ . Но когда он сдавал работу наборщику, то вспомнил, что печатная машинка не сможет воспроизвести такой символ над буквой. Тогда он вынес эту «крышу» перед именем аргумента, и получилось что-то наподобие:

^ x . x * 10

А наборщик, увидев такой символ, использовал заглавную греческую букву Λ :

Λx . x * 10

Вот так и получилось, лямбда-исчисление.

Эта документация перемещена в архив и не поддерживается.

Самая актуальная документация по Visual Studio 2017: Документация по Visual Studio 2017 .

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

В стандарте ISO C++ демонстрируется простое лямбда-выражение, передаваемое функции std::sort() в качестве третьего аргумента:

#include #include void abssort(float * x, unsigned n) { std::sort(x, x + n, // Lambda expression begins (float a, float b) { return (std::abs(a) < std::abs(b)); } // end of lambda expression ); }

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

    предложение фиксации (в спецификации C++ это lambda-introducer .)

    список параметров (необязательно). (Также именуется как lambda declarator .)

    отключаемая спецификация (необязательно).

    спецификация исключений (необязательно).

    завершающий возвращаемый тип (необязательно).

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

Предложение фиксации

В теле лямбда-выражения могут вводиться новые переменные (в C ++14 ). Кроме того, лямбда-выражения могут использовать, или фиксировать , переменные из окружающей области видимости. Лямбда-выражение начинается с предложения фиксации (в синтаксисе стандарта - lambda-introducer ), в котором указываются фиксируемые переменные, а также способ фиксации: по значению или по ссылке. Доступ к переменным с префиксом с амперсандом (&) осуществляется по ссылке, а к переменным без префикса - по значению.

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

Чтобы задать способ фиксации внешних переменных, на которые ссылается лямбда-выражение, вы можете использовать режим фиксации по умолчанию (в синтаксисе стандарта - capture-default): [&] - все переменные фиксируются по ссылке, [=] - по значению. Можно сначала использовать режим фиксации по умолчанию, а затем применить для определенных переменных другой режим. Например, если тело лямбда-выражения осуществляет доступ к внешней переменной total по ссылке, а к внешней переменной factor по значению, следующие предложения фиксации эквивалентны:

[&total, factor] [&, factor] [=, &total] [&total, =]

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

Если предложение фиксации включает capture-default & , ни один identifier в параметре capture этого предложения фиксации не может иметь форму & identifier . Аналогично, если предложение фиксации включает capture-default = , ни один параметр capture этого предложения фиксации не может иметь форму = identifier . Идентификатор или this не могут использоваться более одного раза в предложении захвата. В следующем фрагменте кода приводится несколько примеров.

struct S { void f(int i); }; void S::f(int i) { [&, i]{}; // OK [&, &i]{}; // ERROR: i preceded by & when & is the default [=, this ]{}; // ERROR: this when = is the default {}; // ERROR: i repeated }

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

template void f(Args... args) { auto x = { return g(args...); }; x(); }

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

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

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

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

Обобщенная фиксация (C++14)

В C++14 вы можете объявлять и инициализировать новые переменные в предложении фиксации. Для этого не требуется, чтобы эти переменные существовали во внешней области видимости лямбда-функции. Инициализация может быть выражена в качестве любого произвольного выражения. Тип новой переменной определяется типом, который создается выражением. Одно из преимуществ этой возможности заключается в том, что в C++14 таким образом можно фиксировать переменные из окружающей области видимости, доступные только для перемещения (например std::unique_ptr), и использовать их в лямбда-выражении.

pNums = make_unique>(nums); //... auto a = () { // use ptr };

Список параметров

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

int y = (int first, int second) { return first + second; };

В C++14 , если используются универсальные параметры, в качестве спецификатора типа можно задействовать ключевое слово auto. Это отдает компилятору команду создать оператор вызова функции в качестве шаблона. Каждый экземпляр ключевого слова auto в списке параметров эквивалентен параметру отдельного типа.

auto y = (auto first, auto second) { return first + second; };

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

Поскольку список параметров является необязательным, можно опустить пустые скобки, если аргументы не передаются в лямбда-выражение и lambda-declarator: не содержит элементы exception-specification , trailing-return-type или mutable .

Отключаемая спецификация

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

Спецификация исключений

Можно использовать спецификацию исключений throw() , чтобы указать, что лямбда-выражение не создает исключений. Как и в случае с обычными функциями, компилятор Visual C++ создает предупреждение , если лямбда-выражение объявляет спецификацию исключения throw() , и тело лямбда-выражения вызывает исключение, как показано ниже:

// throw_lambda_expression.cpp // compile with: /W4 /EHsc int main() // C4297 expected { () throw () { throw 5; }(); }

Тип возвращаемого значения

Возвращаемый тип лямбда-выражения выводится автоматически. Использовать ключевое слово не нужно, если не указывается завершающий возвращающий тип . trailing-return-type похож на часть стандартного метода или функции, содержащую возвращаемый тип. Однако тип возвращаемого значения следует списку параметров, и необходимо включить ключевое слово -> элемента trailing-return-type перед типом возвращаемого значения.

Можно опустить часть возвращаемого типа лямбда-выражения, если тело лямбда-выражения содержит только один оператор return или лямбда-выражение не возвращает значение. Если тело лямбда-выражения содержит один оператор return, компилятор выводит тип возвращаемого значения из типа возвращаемого выражения. В противном случае компилятор выводит следующий тип возвращаемого значения: void . Рассмотрим следующие примеры кода, иллюстрирующие этот принцип.

auto x1 = (int i){ return i; }; // OK: return type is int auto x2 = { return { 1, 2 }; }; // ERROR: return type is void, deducing // return type from braced-init-list is not valid

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

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

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

    Фиксированные переменные из внешней области видимости (см. выше).

    Параметры

    Локально объявленные переменные

    Данные-члены класса (при объявлении внутри класса и фиксации this)

    Любая переменная, которая имеет статическую длительность хранения (например, глобальная переменная)

В следующем примере содержится лямбда-выражение, которое явно фиксирует переменную n по значению и неявно фиксирует переменную m по ссылке.

// captures_lambda_expression.cpp // compile with: /W4 /EHsc #include using namespace std; int main() { int m = 0; int n = 0; [&, n] (int a) mutable { m = ++n + a; }(4); cout << m << endl << n << endl; }

Результат:

5
0 Поскольку переменная n фиксируется по значению, ее значение после вызова лямбда-выражения остается равным 0 . Спецификация mutable позволяет изменять n внутри лямбда-выражения.

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

void fillVector(vector& v) { // A local static variable. static int nextValue = 1; // The lambda expression that appears in the following call to // the generate function modifies and uses the local static // variable nextValue. generate(v.begin(), v.end(), { return nextValue++; }); }

Дополнительные сведения см. разделе .

В следующем примере кода используется функция из предыдущего примера и добавляется пример лямбда-выражения с алгоритмом STL generate_n . Это лямбда-выражение назначает элемент объекта vector сумме предыдущих двух элементов. Ключевое слово mutable используется, чтобы тело лямбда-выражения могло изменять соответствующие копии внешних переменных x и y , захваченные лямбда-выражением по значению. Поскольку лямбда-выражение захватывает исходные переменные x и y по значению, их значения остаются равными 1 после выполнения лямбда-выражения.

NextValue++; }); //WARNING: this is not thread-safe and is shown for illustration only } int main() { // The number of elements in the vector. const int elementCount = 9; // Create a vector object with each element set to 1. vector v(elementCount, 1); // These variables hold the previous two elements of the vector. int x = 1; int y = 1; // Sets each element in the vector to the sum of the // previous two elements. generate_n(v.begin() + 2, elementCount - 2, [=]() mutable throw () -> int { // lambda is the 3rd parameter // Generate current value. int n = x + y; // Update previous two values. x = y; y = n; return n; }); print("vector v after call to generate_n() with lambda: " , v); // Print the local variables x and y. // The values of x and y hold their initial values because // they are captured by value. cout << "x: " << x << " y: " << y << endl; // Fill the vector with a sequence of numbers fillVector(v); print("vector v after 1st call to fillVector(): " , v); // Fill the vector with the next sequence of numbers fillVector(v); print("vector v after 2nd call to fillVector(): " , v); }

Результат:

vector v after call to generate_n() with lambda: 1 1 2 3 5 8 13 21 34
x: 1 y: 1
vector v after 1st call to fillVector(): 1 2 3 4 5 6 7 8 9
vector v after 2nd call to fillVector(): 10 11 12 13 14 15 16 17 18 Дополнительные сведения см. в разделе .

Лямбда-выражения не поддерживаются в следующих управляемых сущностях среды CLR: ref class , ref struct , value class и value struct .

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

Необязательный параметр attribute-specifier-seq не поддерживается в этой версии.

Visual Studio включает следующие функции в дополнение к функциям лямбда-выражений стандарта C++11:

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

    Автоматически выведенные возвращаемые типы для тела лямбда-выражений, которые сложнее, чем { return expression; } , при условии, что все возвращаемые выражения относятся к одному типу. (Эта функция является частью предложенного стандарта C++14.)

Показ:

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

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

Примечания:

  • Мы предполагаем, что у вас есть базовые знания PHP.

Что это за функции такие: лямбда и замыкание?

В PHP 5.3 есть множество интересных возможностей, которые приближают его синтаксис к таким языкам программирования как: JavaScript, Python, Ruby и т.д. И, что более важно, эти возможности полезны и стали популярными.

Лямбда-функции и замыкания — это программные объекты, не обладающие именем, они реализуются там, где потребовалась их помощь. Обратите внимание, лямбда-функции не захватывают контекст, а замыкания предназначены для его захвата. Сразу скажем, обе эти функций в PHP по типу соответствуют классу Closure , и указанные выше отличия не принципиальны для практического использования в PHP. В практике вы заметите только то, что от класса Closure ни наследоваться нельзя, ни экземпляры создавать. Его ввели только для внутреннего использования — для типизации.

Лямбда-функции

  • Научится использовать лямбда функции в качестве параметров других функций

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

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

Представьте, что нам всего в одном месте программы нужно отсортировать массив объектов по некоторому полю, нам не нужно объявлять отдельную функцию, мы воспользуемся лямбда-функцией. В Листинге №1 показано как использовать лямбда-функцию для сортировки массива.

Листинг №1 (строки лямбда-функции подсвечены):

number = $number; } public function getNumber() { return $this->number; } } $aNumber = array(); for($i = 0; $i < 10; $i++) { $oNumber = new naturalNumber(); $oNumber->setNumber(rand(1, 10)); $aNumber = $oNumber; } echo "До сортировки:

";
print_r($aNumber);
echo "
"; usort($aNumber, function($oFirst, $oSecond) { if($oFirst->getNumber() < $oSecond->getNumber()) { return -1; } $result = ($oFirst->getNumber() == $oSecond->getNumber()) ? 0: 1; return $result; }); echo "После сортировки:
";
print_r($aNumber);
echo "
"; ?>

В этом примере происходит заполнение массива $aNumber объектами, которые создаются на основе класса naturalNumber . В каждом объекте хранится случайное число, доступ к которому осуществляется через функции getNumber() и setNumber() . Затем происходит сортировка массива с использованием лямбда-функции. Для usort() необходимо, чтобы вторым параметром была указана функция с двумя аргументами, каждый из которых должен соответствовать по типу элементам сортируемого массива. После заполнения массива и в конце программы происходит вывод массива.

Замыкания

  • Использовать контекст функции

Замыкания тоже «вкусны» по-своему, оттенки их «вкуса» можно заметить не всегда. Их применение в коде похоже по стилю на JavaScript. Замыкания, кроме того что они используют переменные контекста, в котором они определены, обладают ещё одним замечательным свойством — их время жизни может быть больше, чем у той функции, в которой их определили.

Для примера захвата значений переменных, возьмём суммирование массива и возвращение результата в контекст программы. Если точнее, захватываться будет ссылка на переменную, в которой будет происходить суммирование. Для полного счастья, суммировать будем рекурсивно по массиву:), побываем во всех его уголках. Пример программы показан в Листинге №2 :

Листинг №2 (строки замыкания подсвечены):

"; ?>

В этом примере происходит создание строки из многомерного массива, путём склеивания всех его элементов через пробел. Стандартно для склеивания используется функция implode() , но здесь не тот случай — она не склеивает многомерные массивы. Может для кого-то этот пример будет кстати.

Существует особенность, которую нужно учитывать, когда вы используете замыкание в методе (метод — функция, член класса). Оказывается оно (замыкание) не захватывает внутреннюю переменную $this (отвечающую за текущий экземпляр). Для того, чтобы её всё-таки передать в замыкание, нужно использовать локальную переменную, например вот так $self = $this , а затем уже передавать в замыкание use($self) .

Способов применения этим функциям можно найти массу. К примеру, можно создавать проекты в стиле JavaScript, можно продлевать жизнь параметрам и переменным и ещё многое. Будем надеяться, что в PHP 6 этот механизм станет более похож на то, что уже есть в других языках.

Есть вопросы? Добро пожаловать в коментарии.


В новой версии Java 8 наконец-то появились долгожданные лямбда-выражения. Возможно, это самая важная новая возможность последней версии; они позволяют писать быстрее и делают код более ясным, а также открывают дверь в мир функционального программирования. В этой статье я расскажу, как это работает.

Java задумывалась как объектно-ориентированный язык в 90-е годы, когда объектно-ориентированное программирование было главной парадигмой в разработке приложений. Задолго до этого было объектно-ориентированное программирование, были функциональные языки программирования, такие, как Lisp и Scheme, но их преимущества не были оценены за пределами академической среды. В последнее время функциональное программирование сильно выросло в значимости, потому что оно хорошо подходит для параллельного программирования и программирования, основанного на событиях («reactive»). Это не значит, что объектная ориентированность – плохо. Наоборот, вместо этого, выигрышная стратегия – смешивать объектно-ориентированное программирование и функциональное. Это имеет смысл, даже если вам не нужна параллельность. Например, библиотеки коллекций могут получить мощное API, если язык имеет удобный синтаксис для функциональных выражений.

Главным улучшением в Java 8 является добавление поддержки функциональных программных конструкций к его объектно-ориентированной основе. В этой статье я продемонстрирую основной синтаксис и как использовать его в нескольких важных контекстах. Ключевые моменты понятия лямбды:

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

Зачем нужны лямбды?

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

Если вы хотите выполнить действия в отдельном потоке, вы помещаете их в метод run из Runnable , вот так:
class MyRunner implements Runnable { public void run() { for (int i = 0; i < 1000; i++) doWork(); } ... }
Затем, когда вы хотите выполнить этот код, вы создаете экземпляр класса MyRunner . Вы можете поместить экземпляр в пул потоков, или поступить проще и запустить новый поток:
MyRunner r = new MyRunner(); new Thread(r).start();
Ключевым моментом является то, что метод run содержит код, который нужно выполнить в отдельном потоке.

Рассмотрим сортировку с использованием пользовательского компаратора. Если вы хотите отсортировать строки по длине, а не по умолчанию, вы можете передать объект Comparator в метод sort:
class LengthStringComparator implements Comparator { public int compare(String firstStr, String secondStr) { return Integer.compare(firstStr.length(),secondStr.length()); } } Arrays.sort(strings, new LengthStringComparator ());
Метод sort все так же вызывает метод compare , переставляя элементы, если они стоят не по порядку, пока массив не будет отсортирован. Вы предоставляете методу sort фрагмент кода, необходимый для сравнения элементов, и этот код встраивается в остальную часть логики сортировки, которую вам, вероятно, не нужно переопределять. Обратите внимание, что вызов Integer.compare (х, у) возвращает ноль, если х и у равны, отрицательное число, если х < у, и положительное число, если х > у. Этот статический метод был добавлен в Java 7. Вы не должны вычислять х – y, чтобы сравнивать х и у, потому что расчет может вызвать переполнение для больших операндов противоположного знака.

В качестве другого примера отложенного выполнения рассмотрим коллбэк для кнопки. Вы помещаете действие обратного вызова в метод класса, реализующего интерфейс слушателя, создаете экземпляр, и регистрируете экземпляр. Это настолько распространенный сценарий, что многие программисты используют синтаксис «анонимный экземпляр анонимного класса»:
button.setOnAction(new EventHandler() { public void handle(ActionEvent event) { System.out.println("The button has been clicked!"); } });
Здесь важен код внутри метода handle . Этот код выполняется всякий раз, когда нажимается кнопка.

Поскольку Java 8 позиционирует JavaFX в качестве преемника инструментария Swing GUI, я использую JavaFX в этих примерах. Детали не имеют значения. В каждой библиотеке пользовательского интерфейса, будь то Swing, JavaFX или Android, вы передаете кнопке некоторый код, который вы хотите запустить, когда кнопка нажата.

Во всех трех примерах вы видели один и тот же подход. Блок кода кому-то передавался - пулу потоков, методу сортировки или кнопке. Этот код вызывался некоторое время спустя.

До сих пор передача кода не была простой в Java. Вы не могли просто передать блоки кода куда угодно. Java является объектно-ориентированным языком, так что вы должны были создать объект, принадлежащий к классу, у которого есть метод с нужным кодом.
В других языках можно работать с блоками кода непосредственно. Проектировщики Java сопротивлялись добавлению этой функции в течение длительного времени. В конце концов, большая сила Java в ее простоте и последовательности. Язык может стать крайне беспорядочным, если будет включать в себя все функции, которые дают чуть более краткий код. Тем не менее, в тех других языках, это не просто легче порождать поток или зарегистрировать обработчик кнопки щелчка; многие их API проще, более последовательны и мощные. В Java, можно было бы написать подобные интерфейсы, которые принимают объекты классов, реализующих определенную функцию, но такие API было бы неудобно использовать.

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

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

Рассмотрим предыдущий пример сортировки еще раз. Мы передаем код, который проверяет, какая строка короче. Мы вычисляем
Что такое firstStr и secondStr ? Они оба строки! Java является строго типизированным языком, и мы должны указать типы:
(String firstStr, String secondStr) -> Integer.compare(firstStr.length(),secondStr.length())

Вы только что видели ваше первое лямбда-выражение! Такое выражение является просто блоком кода вместе со спецификацией любых переменных, которые должны быть переданы в код.

Почему такое название? Много лет назад, когда еще не было никаких компьютеров, логик Алонзо Чёрч хотел формализовать, что значит для математической функции быть эффективно вычисляемой. (Любопытно, что есть функции, которые, как известно, существуют, но никто не знает, как вычислить их значения.) Он использовал греческую букву лямбда (λ), чтобы отметить параметры. Если бы он знал о Java API, он написал бы что-то не сильно похожее на то, что вы видели, скорее всего.

Почему буква λ? Разве Чёрч использовал все буквы алфавита? На самом деле, почтенный труд Principia Mathematica использует символ ˆ для обозначения свободных переменных, которые вдохновили Чёрча использовать заглавную лямбда (Λ) для параметров. Но, в конце концов, он переключился на строчной вариант буквы. С тех пор, выражение с переменными параметрами было названо «лямбда-выражение».

Вы только что видели одну форму лямбда-выражений в Java: параметры, стрелку -> и выражение. Если код выполняет вычисление, которое не вписывается в одно выражение, запишите его так же, как вы бы написали метод: заключенный в {} и с явными выражениями return . Например,
(String firstStr, String secondStr) -> { if (firstStr.length() < secondStr.length()) return -1; else if (firstStr.length() > secondStr.length()) return 1; else return 0; }
Если лямбда-выражение не имеет параметров, вы все равно ставите пустые скобки, так же, как с методом без параметров:
() -> { for (int i = 0; i < 1000; i++) doWork(); }
Если типы параметров лямбда-выражения можно вывести, можно опустить их. Например,
Comparator comp = (firstStr, secondStr) // Same as (String firstStr, String secondStr) -> Integer.compare(firstStr.length(),secondStr.length());
Здесь компилятор может сделать вывод, что firstStr и secondStr должны быть строками, потому что лямбда-выражение присваивается компаратору строк. (Мы посмотрим на это присваивание повнимательнее позже.)

Если метод имеет один параметр выводимого типа, вы можете даже опустить скобки:
EventHandler listener = event -> System.out.println("The button has been clicked!"); // Instead of (event) -> or (ActionEvent event) ->

Вы можете добавить аннотации или модификатор final к параметрам лямбды таким же образом, как и для параметров метода:
(final String var) -> ... (@NonNull String var) -> ...
Вы никогда не указываете тип результата лямбда-выражения. Это всегда выясняется из контекста. Например, выражение
(String firstStr, String secondStr) -> Integer.compare(firstStr.length(), secondStr.length())
может быть использовано в контексте, где ожидается результат типа int .

Обратите внимание, что лямбда-выражение не может возвращать значение в каких-то ветках, а в других не возвращать. Например, (int x) -> { if (x <= 1) return -1; } является недопустимым.

Функциональные интерфейсы

Как мы уже обсуждали, в Java есть много существующих интерфейсов, которые инкапсулируют блоки кода, такие, как Runnable или Comparator . Лямбда-выражения имеют обратную совместимость с этими интерфейсами.

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

Вы можете удивиться, почему функциональный интерфейс должен иметь единственный абстрактный метод. Разве не все методы в интерфейсе абстрактные? На самом деле, всегда было возможно для интерфейса переопределить методы класса Object , например, toString или clone , и эти объявления не делают методы абстрактными. (Некоторые интерфейсы в Java API переопределяют методы Object , чтобы присоединить javadoc-комментарии. Посмотрите Comparator API для примера.) Что еще более важно, как вы вскоре увидите, в Java 8 интерфейсы могут объявлять неабстрактные методы.

Чтобы продемонстрировать преобразование в функциональный интерфейс, рассмотрим метод Arrays.sort . Его второй параметр требуется экземпляр Comparator , интерфейса с единственным методом. Просто предоставьте лямбду:
Arrays.sort(strs, (firstStr, secondStr) -> Integer.compare(firstStr.length(), secondStr.length()));
За кулисами, метод Arrays.sort получает объект некоторого класса, реализующего Comparator . Вызов метода compare на этом объекте выполняет тело лямбда-выражения. Управление этими объектами и классами полностью зависит от реализации, и это может быть что-то гораздо более эффективное, чем использование традиционных внутренних классов. Лучше всего думать о лямбда-выражении как о функции, а не об объекте, и признать, что он может быть передан функциональному интерфейсу.

Это преобразование в интерфейсы – это то, что делает лямбда-выражения настолько мощными. Синтаксис короткий и простой. Вот еще один пример:
button.setOnAction(event -> System.out.println("The button has been clicked!"));
Этот код очень легко читать.

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

Java API определяет несколько универсальных функциональных интерфейсов в пакете java.util.function. Один из интерфейсов, BiFunction , описывает функции с типами Т и U и типом возвращаемого значения R. Вы можете сохранить вашу лямбду сравнения строк в переменной этого типа:
BiFunction compareFunc = (firstStr, secondStr) -> Integer.compare(firstStr.length(), secondStr.length());
Тем не менее, это не поможет вам с сортировкой. Не существует метода Arrays.sort , который принимает BiFunction . Если вы использовали функциональный язык программирования и прежде, вы можете найти это любопытным. Но для Java программистов это довольно естественно. Такой интерфейс, как Comparator , имеет конкретную цель, а не просто метод с заданным параметром и возвращаемым типом. Java 8 сохраняет этот стиль. Если вы хотите сделать что-то с лямбда-выражениями, вы все еще должны понимать назначение этого выражения, и иметь конкретный функциональный интерфейс для этого.

Интерфейсы из java.util.function используются в нескольких Java 8 интерфейсах API , и вы, вероятно, увидите их в других местах в будущем. Но имейте в виду, что вы можете одинаково хорошо преобразовать лямбда-выражение в функциональный интерфейс, который является частью любого API, который вы используете сегодня. Кроме того, вы можете пометить любой функциональный интерфейс с помощью аннотации @FunctionalInterface . Это имеет два преимущества. Компилятор проверяет, что аннотированная сущность представляет собой интерфейс с одним абстрактным методом. И страница Javadoc включает в себя утверждение, что ваш интерфейс является функциональным интерфейсом. Вы не обязаны использовать аннотацию. Любой интерфейс с одним абстрактным методом является, по определению, функциональным интерфейсом. Но использование аннотации @FunctionalInterface - это хорошая идея.

Наконец, заметим, что checked исключения могут возникнуть при преобразовании лямбды в экземпляр функционального интерфейса. Если тело лямбда-выражения может бросить checked исключение, это исключение должно быть объявлено в абстрактном методе целевого интерфейса. Например, следующее было бы ошибкой:
Runnable sleepingRunner = () -> { System.out.println("…"); Thread.sleep(1000); }; // Error: Thread.sleep can throw a checkedInterruptedException
Поскольку Runnable.run не может бросить исключение, это присваивание является некорректным. Чтобы исправить ошибку, у вас есть два варианта. Вы можете поймать исключение в теле лямбда-выражения. Или вы можете присвоить лямбду интерфейсу, один абстрактный метод которого может бросить исключение. Например, метод call из интерфейса Callable может бросить любое исключение. Таким образом, вы можете присвоить лямбду Callable (если добавить return null).

Ссылки на методы

Иногда уже есть метод, который осуществляет именно те действия, которые вы хотели бы передать в другое место. Например, предположим, что вы просто хотите распечатать объект события event , когда кнопка нажата. Конечно, вы могли бы вызвать
button.setOnAction(event -> System.out.println(event));
Было бы лучше, если бы вы могли просто передать метод println в метод setOnAction . Примерно так:
button.setOnAction(System.out::println);
Выражение System.out::println является ссылкой на метод, который эквивалентен лямбда-выражению x -> System.out.println(x) .

В качестве другого примера, предположим, что вы хотите отсортировать строки независимо от регистра букв. Вы можете написать такой код:
Arrays.sort(strs, String::compareToIgnoreCase)
Как вы можете видеть из этих примеров оператор:: отделяет имя метода от имени объекта или класса. Есть три основных варианта:

  • object::instanceMethod
  • Class::staticMethod
  • Class::instanceMethod

В первых двух случаях ссылка на метод эквивалентна лямбда-выражению, которое предоставляет параметры метода. Как уже упоминалось, System.out::println эквивалентно x -> System.out.println(x) . Точно так же, Math::pow эквивалентно (x, y) -> Math.pow(x, y) . В третьем случае первый параметр становится целевым объектом метода. Например, String::compareToIgnoreCase - это то же самое, что и (x, y) -> x.compareToIgnoreCase(y) .

При наличии нескольких перегруженных методов с тем же именем компилятор попытается найти из контекста, какой вы имеете в виду. Например, есть два варианта метода Math.max , один для int и один для double . Какой из них будет вызван, зависит от параметров метода функционального интерфейса, к которому Math.max преобразуется. Так же, как и лямбда-выражения, ссылки на методы не живут в изоляции. Они всегда преобразуются в экземпляры функциональных интерфейсов.

Вы можете захватить параметр this в ссылке на метод. Например, this::equals – это то же, что и x -> this.equals(x) . Можно также использовать super . Выражение super::instanceMethod использует this в качестве цели и вызывает версию данного метода суперкласса. Вот искусственный пример, который демонстрирует механизм:
class Speaker { public void speak() { System.out.println("Hello, world!"); } } class ConcurrentSpeaker extends Speaker { public void speak() { Thread t = new Thread(super::speak); t.start(); } }
При запуске потока вызывается его Runnable , и super::speak выполняется, вызывая speak суперкласса. (Обратите внимание, что во внутреннем классе вы можете захватить эту ссылку из класса приложения, как EnclosingClass.this::method или EnclosingClass.super::method .)

Ссылки на конструктор

Ссылки на конструктор такие же, как ссылки на метод, за исключением того, что именем метода является new . Например, Button::new является ссылкой на конструктор класса Button . На какой именно конструктор? Это зависит от контекста. Предположим, у вас есть список строк. Затем, вы можете превратить его в массив кнопок, путем вызова конструктора для каждой из строк, с помощью следующего вызова:
List strs = ...; Stream

THE BELL

Есть те, кто прочитали эту новость раньше вас.
Подпишитесь, чтобы получать статьи свежими.
Email
Имя
Фамилия
Как вы хотите читать The Bell
Без спама