Функциональные типы и лямбды в Kotlin #
Васильев Андрей Михайлович, 2024
Версии презентации
Функции #
- В Kotlin функции являются объектами первого класса, т.е. их можно сохранять в переменные, получать в качестве аргументов и возвращать в качестве результатов фукнции
- Т.е. с функциями можно работать любым способом, доступным для других значений
- Функцию, помещённую в переменную можно вызвать как и обычную функцию,
()
- Для представления того, что значение является функцией необходимо описать её
как функциональный тип:
(Int) -> Int
fun doSome(value: Int, modifier: (Int) -> Int): Int {
val result: Int = modifier(value)
return result
}
Функциональный тип вызывается с помощью оператора invoke
- Либо явно:
modifier.invoke(55)
- Либо неявно:
modifier(33)
Функциональные типы #
- У всех функциональных типов есть список с типами параметров, заключённых в
скобки, и возвращаемый тип, записываемый после стрелки
(A, B) -> C
(Int, Double) -> String
— функциональный тип, принимающий два числа и возвращающий строку() -> Unit
— функциональный тип, не принимающий никаких аргументов и ничего не возвращающий.Unit
не может быть опущен
- Объявление функционального типа также может включать именованные параметры:
(x: Int, y: Int) -> Point
- Чтобы указать, что функциональный тип может принимать
null
-значения, необходимо использовать круглые скобки:((Int, Int) -> Int)?
- Вы также можете присвоить функциональному типу альтернативное имя, используя
псевдонимы типов
typealias ClickHandler = (Button, ClickEvent) -> Unit fun addClickHandler(handler: ClickHandler) { ... }
Создание функционального типа #
Если мы хотим создать новый блок кода, который можно сохранить (или передать):
- Используя лямбда-выражение:
{ a, b -> a + b}
- Используя анонимную функцию:
fun(a: Int, b: Int): Int = a + b
В лямбда-выражении можно опустить типы, если компилятор сможет корректно их вывести относительно места их использования
val result = doSome(
34,
{ num -> num + 42 },
)
println(result)
Помимо создания нового блока можно создать ссылку на существующую функцию
Синтаксис лямбда-выражений #
Полная синтаксическая форма лямбда-выражений может быть представлена следующим образом:
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
- Лямбда-выражение всегда заключено в скобки
{...}
- Объявление параметров при таком синтаксисе происходит внутри этих скобок и может включать в себя аннотации типов
- Тело функции начинается после знака
->
- Тело может состоять из нескольких выражений как и обычная функция
- Если тип возвращаемого значения не
Unit
, то в качестве возвращаемого типа принимается последнее (а возможно и единственное) выражение внутри тела лямбды
Если вы вынесите все необязательные объявления, то, что останется, будет выглядеть следующим образом:
val sum = { x: Int, y: Int -> x + y }
Возвращение значения из лямбда-выражения #
- Вы можете вернуть значение из лямбды явно, используя оператор
return
- Либо неявно будет возвращено значение последнего выражения
Таким образом, два следующих фрагмента равнозначны:
ints.filter {
val shouldFilter = it > 0
shouldFilter
}
ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter
}
- Вторую форму удобно использовать в случае, если возможно несколько логических выходов из одного блока
- Блоку кода также может быть присвоена метка, что удобно при работе с вложенными лямбда-выражениями
ints.filter positive@{
val shouldFilter = it > 0
return@positive shouldFilter
}
return
без метки совершит выход из функции, не из лямбда-выражения
Передача лямбды в качестве последнего параметра #
В Kotlin существует соглашение: если последний параметр функции является функцией, то лямбда-выражение, переданное в качестве соответствующего аргумента, может быть вынесено за круглые скобки
val product = items.fold(1) { acc, e -> acc * e }
Такой синтаксис также известен как trailing lambda
Когда лямбда-выражение является единственным аргументом функции, круглые скобки могут быть опущены
run { println("...") }
Стандартная библиотека по работе с коллекциями предлагает много функций, которые предполагают работу с лямбда-выражениями
it
— неявное имя единственного параметра
#
- Очень часто лямбда-выражение имеет только один параметр
- Если компилятор способен самостоятельно определить сигнатуру, то объявление
параметра можно опустить вместе с
->
- Параметр будет неявно объявлен под именем
it
ints.filter { it > 0 } // этот литерал имеет тип '(it: Int) -> Boolean'
Символ подчеркивания для неиспользуемых переменных #
Если параметр лямбды не используется, то разрешено его имя заменить на символ подчёркивания
map.forEach { _, value -> println("$value!") }
Замыкания #
- Лямбда-выражение или анонимная функция (так же, как и локальная функция или анонимные объекты) имеет доступ к своему замыканию, то есть к переменным, объявленным вне этого выражения или функции
- Переменные, захваченные в замыкании, могут быть изменены в лямбде
var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)
Обычно можно встретить в функциях-генераторах:
typealias Modifier = (Int) -> Int
fun createNumberAdder(howMuch: Int): Modifier = {
number + howMuch
}
Реализация функционального типа через классы #
- Функции-генераторы могут быть эффективно заменены классами, которые создают объекты, согласующиеся с функциональным типом
- В классе необходимо реализовать оператор
invoke
typealias Modifier = (Int) -> Int
class NumberAdder(
private val howMuch: Int,
): Modifier {
override fun invoke(number: Int): Int {
return number + howMuch
}
}
val twoAdder = NumberAdder(2)
println(twoAdder(42)) // "44"
Функциональные (SAM) интерфейсы #
- Интерфейсы только с одним абстрактным методом называются функциональными интерфейсами или Single Abstract Method (SAM) интерфейсами
- Функциональный интерфейс может иметь несколько неабстрактных членов, но только один абстрактный
Чтобы объявить функциональный интерфейс, используйте модификатор fun.
fun interface KRunnable {
fun invoke()
}
Классы, которые его реализуют, должны будут реализовать только одну функцию
Использование SAM-интерфейса #
Рассмотрим следующий SAM-интерфейс по манипулированию целыми числами
fun interface IntManipulator {
fun manipulate(input: Int): Int
}
Реализуем этот интерфейс с помощью лямбда-выражения
val twoMultiplier = IntManipulator { it * 2 }
Реализуем этот интерфейс с помощью классов
class NumberMultiplier(private val multiplicator)
: IntManipulator {
override fun manipulate(input: Int) {
return input * multiplicator
}
}
val twoMultiplier = NumberMultiplier(2)
Функциональные интерфейсы и псевдонимы типов #
Функциональные интерфейсы и псевдонимы типов служат разным целям
- Псевдонимы типов – это просто имена существующих типов, они не создают новый тип, в то время как функциональные интерфейсы делают это
- Вы можете предоставить расширения, специфичные для конкретного функционального интерфейса, которые будут неприменимы для простых функций или их псевдонимов типов
- Псевдонимы типов могут иметь только один элемент, в то время как функциональные интерфейсы могут иметь несколько неабстрактных элементов и один абстрактный элемент
- Функциональные интерфейсы также могут реализовывать и расширять другие интерфейсы
- Функциональные интерфейсы более гибкие и предоставляют больше возможностей, чем псевдонимы типов, но они могут быть более дорогостоящими как синтаксически, так и во время выполнения, поскольку могут потребовать преобразования в определенный интерфейс