Введение в анонимные функции и лямбды
В языке программирования Kotlin есть анонимные функции, а есть лямбды, которые тоже иногда называют анонимными функциями. Сейчас объясню почему.
Анонимные функции – это практически классические функции, которые имеют собственное расширенное поведение, умеют принимать и возвращать параметры, но не имеют имени. Объявляются они точно также с помощью ключевого слова fun, но без указания названия.
На практике такая функция может быть использована прямо в момент объявления. Например, когда мы в приложении сделали кнопку, повесили на нее прослушиватель нажатий и прописали инструкции в нем. Этими инструкциями может быть тут же объявленная анонимная функция и каким-то действием внутри, которое нужно совершить при клике на кнопку. Об этом механизме и о коллбэках будем говорить подробнее в рамках Android.
Окей. Создадим функцию, которая возвращает количество оставшихся дней до нового года. Это может применяться в массе случаев в рамках функционала какого-то приложения. Сначала создаем инстанс объекта даты. Инстанс – это то же самое, что и экземпляр класса. Воспользуемся абстрактным классом Calendar и вызовем у него функцию getInstance() – она вернет актуальный для текущего часового пояса и региона календарь со всеми основными данными о датах, времени и всего такого. Далее пишем саму функцию без имени. В скобках сразу пишутся принимаемые параметры, но пусть сейчас входных параметров не будет и скобки оставляем пустыми.
Анонимуную функцию также можно писать в короткой и развернутой форме. Сначала напишем реализацию в теле, в фигурных скобках. Возвращать будем разницу количества дней в году и текущего дня с начала года, то есть пишем возвращемый тип Int. Текущий день берем из объекта календаря. Отсчет первого дня года начинается с единицы, а не с нуля, так что дополнительных манипуляций не понадобится.
fun main() {
val calendar: Calendar = Calendar.getInstance()
fun(): Int {
return 365 - calendar[Calendar.DAY_OF_YEAR]
}
}
Ок, но изящнее эта функция все таки выглядит в одну строку, так и сделаем.
fun() = 365 - calendar[Calendar.DAY_OF_YEAR]
Передача параметров в лямбда-выражения
Анонимную функцию можно сохранять в переменную. Еще можно передавать ее в другую функцию как параметр, если параметр соответствует типу этой функции. Также и как с обычными данными, анонимную функцию можно возвращать из другого метода.
Рассмотрим пример с переменной. Создаем переменную и присвоим ей функцию. На всякий случай обращу ваше внимание, что не нужно путать с сохранением получаемого значения, которое возвращает классическая функция. Здесь мы сохраняем сам метод. Чтобы это увидеть наглядно, принудительно выведем тип новой переменной. Слева от стрелки указывается тип принимаемых параметров (сейчас отсутствуют), справа возвращаемый тип – Int.
val getDaysToEndYear: () -> Int = fun() = 365 - calendar[Calendar.DAY_OF_YEAR]
С этой переменной можем уже взаимодействовать, как с упакованным методом. Чтобы наконец вызвать функцию и распечатать значение к ней необходимо или применить invoke(), что дословно переводится, как вызывать. Или просто проставить круглые скобки. Сегодня 14 декабря и до Нового года 17 дней – все верно.
fun main() {
val calendar: Calendar = Calendar.getInstance()
val getDaysToEndYear: () -> Int =
fun() = 365 - calendar[Calendar.DAY_OF_YEAR]
println(getDaysToEndYear.invoke())
}
Создадим еще одну функцию для примера. Сразу в объявленную переменную convertEndDaysToMills. Метод не будет ничего возвращать, а только выполнять внутри определенные вычисления и печатать их. На вход передаем остаток количества дней до конца года, внутри вызываем println() с такой формулой: 1000 миллисекунд * 60 секунд * 60 минут * 24 часа * количество дней до конца года. Вызываем этот метод с параметрами из предыдущей анонимной функции и получаем конвертацию дней в миллисекундах.
Давайте тоже здесь проставим тип переменной и увидим следующее (Int) -> Unit
. На этот раз слева от стрелки в круглых скобках тип входных параметров (в предыдущей функции они отсутствовали). А справа тип Unit, о котором я упоминал на прошлом уроке. Этот тип означает, что функция ничего не возвращает.
val convertEndDaysToMills: (Int) -> Unit =
fun(endDays: Int) = println(1000 * 60 * 60 * 24 * endDays)
convertEndDaysToMills(getDaysToEndYear())
Лямбда выражения
Хорошо, эта информация понадобится для понимания сути лямбд. Лямбда выражения – это такой вид объекта, который содержит какой-то блок кода. Как правило, это небольшая часть кода, содержащая определенную функциональность или выполняющее какое-то действие. По сути ее можно называть функцией (даже анонимной функцией).
Теперь тезисно (это можно конспектировать):
- Лямбды можно считать за функции, а именно за анонимные функции (у которых нет названия). Разница между анонимными функциями и лямбдами заключается в том, что анонимы без названия объявляются с помощью ключевого слова fun. Лямбды же объявляются с помощью заключения кода в фигурные скобки. Из примеров станет понятно, а пока основное это – fun не используется.
- Лямбды можно также присваивать переменным, передавать их и возвращать из обычных функций. А это значит, что мы можем получать или передавать в любое желаемое место не просто данные в виде базовых типов данных или объектов, но и функциональность. То есть можно передать в другую функцию какое-то поведение. Чтобы это действие выполнилось внутри другого метода. Такой же функционал и у анонимных функций, описанных выше.
- В лямбдах не используется ключевое слово return для возврата данных. Результат всегда автоматически возвращается из последней прописанной строки (или инструкции) в теле лямбды.
Реализация
Теперь будем связывать воедино. Но пойдем немного от обратного и небольшими шагами.
- Сначала создадим переменную для хранения лямбды с обязательным указанием типа. Лямбда не будет ничего принимать и возвращать. Вспоминаем тип анонимной функции выше и пишем этот тип.
- Теперь проинициализируем эту переменную лямбдой. Пишем переменную, знак присваивания и открываем фигурные скобки. Вспоминаем тезис, что лямбды объявляются заключением кода в фигурные скобки. Внутри просто пишем println() с произвольным текстом.
- Наконец, вызываем переменную с лямбдой. Напомню вызовать можно функцию как указав пустые скобки (если нет параметров), так и с помощью invoke().
val printStringWithLambda: () -> Unit
printStringWithLambda = {
println("print string with lambda")
}
printStringWithLambda()
Мы создали простейшую лямбду. Поработаем с ней. Лямбду можно создать и вызвать с помощью круглых скобок, как обычную функцию, в одной строке. Проставляем скобки лямбда-выражения и внутри, в теле прописываем нужное действие. Повторим предыдущий код в сокращенной форме.
// короткая запись
{ println("print string with lambda") }()
И все покрасилось. Это произошло потому, что текущий код Kotlin интерпретирует как дополнительный параметр для функции выше, ввиду того, что код начался с открытия фигурной скобки. Чтобы было понятно, продублирую этот код сверху.
printStringWithLambda() {
println("print string with lambda")
}()
Такая же ошибка. Подобный пример относится к разряду Пазлеров – когда код воспринимается одним образом, но делает что-то неожиданное. Чтобы это починить, разделяем функции, поставив после printStringWithLambda() точку с запятой. Тогда они не будут аффектить друг-друга.
Объяснение сигнатуры лямбды
Ок. Раздуваем лямбду дальше. Добавим какой-то входной целочисленный параметр и также применим его в теле. Начнем также с добавления типа параметра в объявлении переменной, затем в развернутой форме, затем в сокращенной форме записи.
val printStringWithLambda: (Int) -> Unit
printStringWithLambda = {
println("print string with lambda $it")
}
printStringWithLambda(42);
// короткая запись
{ it: Int -> println("print string with lambda $it" )}(442)
Что тут появилось:
- там, где объявили переменную в типе лямбды в круглых скобках указали принимающий параметр;
- далее в месте инициализации лямбды сразу после открытия фигурной скобки среда разработки подсвечивает it: Int. it – появляется, когда анонимная функция (или лямбда) принимает только один параметр и компилятор способен опрелелить его сигнатуру. Тогда этот параметр неявно объявляется под именем it и это имя (переменную) можно использовать в теле. Как мы и сделали, передав значение в строку. Вспоминаем аналогию с входными параметрами классической функции, здесь то же самое, но выглядит иначе. Обратите внимание, нам доступны опции по оформлению этой сигнатуры. Например, можно явно вывести значение сигнатуры с помощью контекстного меню. Стрелка в этом случае всегда обязательна. Теперь эта сигнатура похожа на прописанный тип выше. Слева от стрелки входящее значение, справа само значение. it можно переименовать на свой вкус, обычно так и делается, чтобы понимать предназначение объекта, что хранится внутри.
- наконец, вызываем переменную с лямбдой с параметром 42;
- в короткой записи мы создали лямбду, дополнив сигнатуру it, на этот раз обязательно. И тут же вызвали лямбду с каким-то целочисленным значением. Вызывать по прежнему можно либо круглыми скобками, либо через invoke().
Хорошо. Снова модернизируем сигнатуру. Вообще, немного поясню, я выбрал такой подход, чтобы начинать накапливать “насмотренность” на лямбды в разных их вариациях. Это поможет не шарахаться от более сложных конструкций и ускорить их читабельность. Ведь подовляющее большинство сложно читаемых конструкций – это всевозможные вызовы лямбд в паре с экстеншн функциями. А так как вы дошли уже до этого урока/момента, это говорит о вашем настрое во всем разобраться досконально и добиться желаемого результата. Декомпозиция задачи всегда работает на нас в любой сфере. И иногда полезно явно отображать типы в лямбде и присваивать ее переменной. Тем более, что IDEA или Android Studio нам в этом помогает через контекстное меню. Продолжим.
Теперь будем возвращать из лямбды значение. Далеко ходить не будем, вернем строку, которую печатаем:
- в типе меняем на возвращаемое значение String;
- в теле убираем println() – напомню, чтобы вернуть значение из лямбды не нужно писать ключевое слово return. По умолчанию всегда возвращается выражение на последней строке в теле;
- в вызове все без изменений;
- в короткой записи также убираем println() и.. все. Здесь просто создастся лямбда-выражение, выполнится и никуда не вернет строку, потому, что некуда.
val printStringWithLambda: (Int) -> String
printStringWithLambda = {it: Int ->
"print string with lambda $it"
}
printStringWithLambda(42);
// короткая запись
{ it: Int -> "print string with lambda $it" }.invoke(442)
Сравнение анонимных функций и лямбда-выражений
Чтобы полностью закрыть сравнение анонимных функций с лямбда-выражениями, воспроизведем пример из начала урока. Создадим переменную и сохраним в нее лямбду, которая при выполнении будет считать и сразу распеатывать сконвертированное значение. Затем вызываем, отправляя в нее результат анонимной функции по вычислению оставшего количества дней до Нового года. И для порядка все же распечатаем то, что вернут предыдущие выражения.
val printStringWithLambda: (Int) -> String
printStringWithLambda = { it: Int ->
"print string with lambda $it"
}
println(printStringWithLambda(42));
// короткая запись
println({ it: Int->"print string with lambda $it" }.invoke(442))
// конвертация дней в миллисекунды
val convertLambda = { endDays: Int ->
println("Convert from lambda: ${1000 * 60 * 60 * 24 * endDays}")
}
convertLambda(getDaysToEndYear())
Все работает.
Для тех, кто собрался стать Android-разработчиком
Пошаговая
схема
Описание процесса обучения от основ Kotlin до Android-разработчика
Бесплатные
уроки
Авторский бесплатный курс по основам языка программирования Kotlin
Обучающий
бот
Тренажер и самоучитель по Котлин – бесплатные тесты и практика