Lambda в Kotlin — Подробное Руководство

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

Содержание статьи

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

Основы лямбда-выражений в Kotlin

Лямбда также известна как анонимная функция. Она получила свое название от лямбда-исчисления Алонзо Черча, в котором все функции анонимны. Лямбда также является синонимом замыкания (closures). Лямбда-выражения используются и в других языках программирования, такие как Python, например.

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

Вы можете спросить — если лямбды являются функциями без названий, то как их использовать?

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

Далее дано объявление переменной, которая может содержать лямбду:

Lambda multiplyLambda принимает два значения типа Int и возвращает Int. Обратите внимание, что это то же самое, что объявление переменной для функции. Как было сказано, лямбда — это просто функция без названия. Типом лямбды является тип функции.

Лямбда присваивается переменной следующим образом:

Это похоже на объявление функции, но есть небольшие отличия. Здесь тот же список параметров, но символ -> показывает тип возвращаемого значения. Тело лямбды начинается после возвращаемого типа. Лямбда-выражение возвращает значение последнего выражения в теле.

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

Как и следовало ожидать, lambdaResult равен 8. И вновь есть небольшая разница. Лямбда не разрешает использовать названия для аргументов. К примеру, вы не можете написать multiplyLambda(a = 4, b = 2). В отличие от обычных функций, вы не можете использовать именованные аргументы при вызове lambda.

Сокращенный синтаксис для lambda-выражения

По сравнению с функцией, лямбда является более короткой и легковесной. Есть много способов сократить её синтаксис. К примеру, вы можете использовать выведение типа Kotlin, убирая тем самым информацию о типе:

Помните, что вы уже объявили переменную multiplyLambda как лямбду, принимающую два значения Int и возвращающую одно значение Int, так что вы можете позволить Kotlin определить данные типы за вас.

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

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

Так как здесь только один параметр и тип лямбды определен, синтаксис её применения может быть сокращен до такого вида:

Код выше считается устаревшим. Применение it при работе с лямбда-выражением выглядит следующим образом:

Лямбда в качестве аргумента для функции

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

Код объявляет функцию operateOnNumbers, которая принимает первые два параметра типа Int. Третий параметр называется operation, и он имеет тип функции.

Функция operateOnNumbers возвращает результат который имеет тип Int.

Вы можете вызвать функцию operateOnNumbers с лямбдой в качестве третьего аргумента следующий образом:

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

Функция operateOnNumbers вызывается одинаково, будь значение из operation функцией или лямбдой. Оператор :: является ссылочным оператором. В данном случае он дает указание коду найти функцию addFunction в текущей области видимости.

Сила лямбда-синтаксиса нам снова пригодится.

Можно определить встроенную лямбду с помощью вызова функции operationOnNumbers следующим образом:

Не нужно определять лямбду и назначать её локальной переменной или константе. Можно просто объявить лямбду прямо там, где вы передаете ее в функцию в качестве аргумента!

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

Можно пойти дальше. Оператор + является оператором функции plus() от класса Int, которая принимает два аргумента и возвращает один результат, поэтому вы можете написать:

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

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

Лямбды без возвращаемого значения

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

Типом лямбды является () -> Unit. Пустые скобки означают отсутствие параметров. Вы должны объявить возвращаемый тип, чтобы Kotlin знал, что объявляется лямбду. Именно здесь пригодится тип Unit, когда лямбда не должна возвращать никакого важного значения.

Если требуется, чтобы лямбда не возвращала никакого значение, то можно использовать тип Nothing. К примеру:

Поскольку появляется исключение NullPointerException, лямбда фактически не возвращает значение.

Область видимости замыкания (Closure)

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

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

Рассмотрим следующую лямбду:

Лямбда incrementCounter довольно простая: она увеличивает переменную counter на одну единицу. Переменная counter определяется вне лямбды. Лямбда может получить доступ к переменной, потому что лямбда определена в той же области, что и переменная. В таком случае, лямбда захватывает переменную counter. Любые изменения, которые вносятся в переменную, видны как внутри лямбды, так и за её пределами.

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

После данных пяти вызовов — переменная counter будет равна 5.

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

Данная функция не принимает параметров и возвращает лямбду. Возвращаемая лямбда не принимает параметров и возвращает Int.

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

К примеру, это можно было бы реализовать следующим образом:

Два экземпляра элемента counter, созданные функцией, являются взаимно исключающими и считаются независимо друг от друга — так как имеют разные области видимости. Здорово!

Сортировка данных при помощи Lambda

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

Вы вызываете метод sort() для получения отсортированного массива следующим образом:

Результат:

Указав нашу собственную лямбду методу compareBy() в качестве функции для сортировки, которая возвращает Comparator для метода sortedWith(), можно изменить условия сортировки массива.

Передайте лямбду в метод compareBy() следующим образом:

Результат:

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

Итерация по коллекциям с лямбда-выражениями

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

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

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

Результат:

Здесь выполняется перебор каждого элемента из коллекции и выводится его значение в квадрате.

Другая функция позволяет отфильтровать определенные элементы которые соответствую определенному условию:

Здесь создается список значений типа Double для обозначения цен товаров в магазине. Для фильтрации цен на продукты которые превышают 5 долларов можно использовать функцию filter.

Данная функция выглядит следующим образом:

Это означает, что функция filter принимает один параметр под названием predicate, который является лямбдой (или функцией), которая принимает T и возвращает Boolean значение. Затем функция filter возвращает список T. В этом контексте T относится к типу элементов в списке. В приведенном выше примере это тип Double.

Задача лямбды которую она выполняет внутри filter это вернуть true или false в зависимости от того, следует ли сохранить значение или пропустить её. Список, возвращаемый из filter, будет содержать все элементы, для которых лямбда вернула true.

В данном примере largePrices будет содержать:

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

Однако это еще не все!

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

Функция map принимает лямбду и выполняет её для каждого элемента в списке и вернет новый список, содержащий элементы которые прошли проверку из лямбды. В этом случае salePrices будет содержать:

На заметку: Не путайте функцию map, используемую для преобразования данных из коллекций, с различными типами Map вроде HashMap или функциями вроде mapOf, которые создают карты.

Изменение типа элементов внутри списка

Функцию map также можно использовать для изменения типа элементов внутри списка. Это делается следующим образом:

Мы имеем в константе userInput небольшой список со строками, и нам нужно превратить их в тип Int? если там действительно числа или в null если там обычный текст.

Если вы хотите отфильтровать данные и избавиться от null значений, тогда можно использовать метод mapNotNull() следующим образом:

Это почти то же самое, что и map, за исключением того, что игнорируются null значения.

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

Её можно использовать над списком price для расчета результата:

Начальное значение равна 0.0. Затем лямбда подсчитывает сумму текущего значения плюс следующий значения из списка. Таким образом, вы вычисляете сумму всех значений в списке. В этом случае значение из переменной sum будет:

Функция reduce тесно связана с fold. В Kotlin reduce использует первый элемент из коллекции как начальное значение:

Теперь, когда вы увидели как работают функции filter, map, fold и reduce, стало ясно, насколько мощными могут быть эти функции, особенно благодаря синтаксису лямбда-выражений. Всего за несколько строк кода мы вычислили некоторые довольно сложные значения из коллекции.

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

В данном случае, параметром функции forEach является Map.Entry, он содержит key и value от элементов карты.

Вот результат:

На этом завершается раздел по изучению способов итерации данных при помощи лямбда-выражений!

Задачи для проверки

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

Задача №1 — Создайте константу nameList, которая содержит список из строк с именами. Подойдут любые имена — убедитесь, что у вас в списке больше трёх имён. Теперь используйте функцию fold, чтобы создать новую строку, которая содержим все имена из списка склеенными вместе.

Задача №2 — Используя тот же список nameList, сначала отфильтруйте список, чтобы он содержал только имена, в которых содержится более четырех символов, а затем создайте такую же склейку из имен, как в приведенном выше задании.

Задача №3 — Создайте константу которая будет содержать карту под названием namesAndAges, которая содержит имена, сопоставленные по возрасту, возраст имеет тип integer. Теперь используйте функцию filter, чтобы создать новую карту, содержащую только людей младше 18 лет.

Задача №4 — Используя карту namesAndAges, отфильтруйте взрослых людей (от 18 лет и старше), а затем используйте функцию map для преобразования её в список, содержащий только имена, то есть можно убрать возраст.

Повторите функцию несколько раз

Задача №5Напишите функцию, которая запускает лямбду task() столько раз сколько будет указано в параметре time.

Объявите функцию следующим образом:

Используйте данную функцию для вывода предложения "Kotlin классный!" 10 раз.

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

Объявите функцию следующим образом:

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

Функция mathSum должна вычислить и вернуть сумму значений от полученному из lambda-выражения результату в зависимости от указанному интервалу из lenght.

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

Задача №7 — В данном задании вам предоставляется список названий приложений с рейтингами которые им давались. Это выдуманные приложения.

Вот как выглядит карта с приложениями:

Сначала создайте новую карту averageRatings, которая содержит названия приложений и их средний рейтинг. Используйте функцию forEach для итерации по карте appRatings, затем используйте функцию reduce для вычисления среднего рейтинга приложений и сохраните результат в карте averageRatings.

В конце, используйте вместе функции filter и map для получения списка названий тех приложений, чей средний рейтинг выше 3.

Ключевые особенности лямбда-выражений

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

Что дальше?

Лямбды и функции являются фундаментальными типами для хранения и повторного использования важных фрагментов кода. Помимо объявления и вызова lambda — вы узнали как их можно передавать в качестве аргумента другим функциям — тем самым расширяя возможности обычным функциям.

В следующей статье мы поговорим о создании своих собственных типов данных.

5 2 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest
1 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Ваня
Ваня
2 лет назад

А где про классы?

Последний раз редактировалось 2 лет назад Ваня ем
1
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x