type | layout | title | url |
---|---|---|---|
doc |
reference |
Лямбды |
В Kotlin функции являются функциями первого класса. Это означает, что они могут храниться в переменных и структурах данных, передаваться в качестве аргументов и возвращаться из других функций высшего порядка. Вы можете работать с функциями любым способом, который возможен для других нефункциональных значений.
Чтобы это облегчить, Kotlin, как статически типизированный язык программирования, использует семейство функциональных типов для представления функций и предоставляет набор специализированных языковых конструкций, таких как лямбда-выражения.
Функция высшего порядка - это функция, которая принимает функции как параметры, или возвращает функцию в качестве результата.
Хорошим примером такой функции является идиома функционального программирования fold
для коллекций, которая принимает начальное значение - accumulator
вместе с комбинирующей функцией и строит возвращаемое значение, последовательно комбинируя текущее значение accumulator
с каждым элементом коллекции, заменяя значение accumulator
:
fun <T, R> Collection<T>.fold(
initial: R,
combine: (acc: R, nextElement: T) -> R
): R {
var accumulator: R = initial
for (element: T in this) {
accumulator = combine(accumulator, element)
}
return accumulator
}
В приведённом выше коде параметр combine
имеет функциональный тип (R, T) -> R
, поэтому он принимает функцию, которая принимает два аргумента типа R
и T
и возвращает значение типа R
.
Он вызывается внутри цикла for и присваивает accumulator
возвращаемое значение.
Чтобы вызвать fold
, мы должны передать ему экземпляр функционального типа в качестве аргумента и лямбда-выражение (описание ниже). Лямбда-выражения часто используются в качестве параметра функции высшего порядка.
fun main() {
val items = listOf(1, 2, 3, 4, 5)
// Лямбда - это блок кода, заключенный в фигурные скобки.
items.fold(0, {
// Если у лямбды есть параметры, то они указываются перед знаком '->'
acc: Int, i: Int ->
print("acc = $acc, i = $i, ")
val result = acc + i
println("result = $result")
// Последнее выражение в лямбде считается возвращаемым значением:
result
})
// Типы параметров в лямбде необязательны, если они могут быть выведены:
val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i })
// Ссылки на функции также могут использоваться для вызовов функций высшего порядка:
val product = items.fold(1, Int::times)
println("joinedToString = $joinedToString")
println("product = $product")
}
Следующие разделы объясняют более подробно концепции, упомянутые выше.
Kotlin использует семейство функциональных типов, таких как (Int) -> String
, для объявлений, которые являются частью функций: val onClick: () -> Unit = ...
.
Эти типы имеют специальные обозначения, которые соответствуют сигнатурам функций, то есть их параметрам и возвращаемым значениям:
- У всех функциональных типов есть список с типами параметров, заключенный в скобки, и возвращаемый тип:
(A, B) -> C
обозначает тип, который предоставляет функции два принятых аргумента типаA
иB
, а также возвращает значение типаC
. Список с типами параметров может быть пустым, как, например, в() -> A
. Возвращаемый типUnit
не может быть опущен.
- У функциональных типов может быть дополнительный тип - получатель (receiver), который указывается в объявлении перед точкой: тип
A.(B) -> C
описывает функции, которые могут быть вызваны для объекта-получателяA
с параметромB
и возвращаемым значениемC
. Литералы функций с объектом-приёмником часто используются вместе с этими типами.
- Останавливаемые функции (suspending functions) принадлежат к особому виду функциональных типов, у которых в объявлении присутствует модификатор suspend, например,
suspend () -> Unit
илиsuspend A.(B) -> C
.
Объявление функционального типа также может включать именованные параметры: (x: Int, y: Int) -> Point
.
Именованные параметры могут быть использованы для описания смысла каждого из параметров.
Чтобы указать, что функциональный тип может быть nullable, используйте круглые скобки:
((Int, Int) -> Int)?
.При помощи круглых скобок функциональные типы можно объединять:
(Int) -> ((Int) -> Unit)
.Стрелка в объявлении является правоассоциативной (right-associative), т.е. объявление
(Int) -> (Int) -> Unit
эквивалентно объявлению из предыдущего примера, а не((Int) -> (Int)) -> Unit
.
Вы также можете присвоить функциональному типу альтернативное имя, используя псевдонимы типов:
typealias ClickHandler = (Button, ClickEvent) -> Unit
Существует несколько способов получить экземпляр функционального типа:
-
Используя блок с кодом внутри функционального литерала в одной из форм:
- лямбда-выражение:
{ a, b -> a + b }
, - анонимная функция:
fun(s: String): Int { return s.toIntOrNull() ?: 0 }
Литералы функций с объектом-приёмником могут использоваться как значения функциональных типов с получателем.
- лямбда-выражение:
-
Используя вызываемую ссылку на существующее объявление:
- функции верхнего уровня, локальной функции, функции-члена или функции-расширения:
::isOdd
,String::toInt
, - свойства верхнего уровня, члена или свойства-расширения:
List<Int>::size
, - конструктора:
::Regex
К ним относятся привязанные вызываемые ссылки, которые указывают на член конкретного экземпляра:
foo::toString
. - функции верхнего уровня, локальной функции, функции-члена или функции-расширения:
- Используя экземпляр пользовательского класса, который реализует функциональный тип в качестве интерфейса:
class IntTransformer: (Int) -> Int {
override operator fun invoke(x: Int): Int = TODO()
}
val intFunction: (Int) -> Int = IntTransformer()
При достаточной информации компилятор может самостоятельно вывести функциональный тип для переменной:
val a = { i: Int -> i + 1 } // Выведенный тип - (Int) -> Int
Небуквальные (non-literal) значения функциональных типов с и без получателя являются взаимозаменяемыми, таким образом получатель может заменить первый параметр, и наоборот. Например, значение типа (A, B) -> C
может быть передано или назначено там, где ожидается A.(B) -> C
, и наоборот.
fun main() {
val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }
val twoParameters: (String, Int) -> String = repeatFun // OK
fun runTransformation(f: (String, Int) -> String): String {
return f("hello", 3)
}
val result = runTransformation(repeatFun) // OK
println("result = $result")
}
Обратите внимание, что функциональный тип без получателя выводится по умолчанию, даже если переменная инициализируется со ссылкой на функцию-расширение. Чтобы это изменить, укажите тип переменной явно.
Значение функционального типа может быть вызвано с помощью оператора invoke(...)
: f.invoke(x)
или просто f(x)
.
Если значение имеет тип получателя, то объект-приёмник должен быть передан в качестве первого аргумента.
Другой способ вызвать значение функционального типа с получателем - это добавить его к объекту-приёмнику, как если бы это была функция-расширение: 1.foo(2)
,
Пример:
fun main() {
val stringPlus: (String, String) -> String = String::plus
val intPlus: Int.(Int) -> Int = Int::plus
println(stringPlus.invoke("<-", "->"))
println(stringPlus("Hello, ", "world!"))
println(intPlus.invoke(1, 1))
println(intPlus(1, 2))
println(2.intPlus(3)) // вызывается как функция-расширение
}
Иногда выгодно улучшить производительность функций высшего порядка, используя встроенные функции.
Лямбда-выражения и анонимные функции - это "функциональный литерал", то есть необъявленная функция, которая немедленно используется в качестве выражения. Рассмотрим следующий пример:
max(strings, { a, b -> a.length < b.length })
Функция max
является функцией высшего порядка, потому что она принимает функцию в качестве второго аргумента.
Этот второй аргумент является выражением, которое в свою очередь есть функция, то есть функциональный литерал.
Как функция он эквивалентен объявлению:
fun compare(a: String, b: String): Boolean = a.length < b.length
Полная синтаксическая форма лямбда-выражений, таких как literals of function types, может быть представлена следующим образом:
val sum = { x: Int, y: Int -> x + y }
Лямбда-выражение всегда заключено в скобки {...}
, объявление параметров при таком синтаксисе происходит внутри этих скобок и может включать в себя аннотации типов (опционально), тело функции начинается после знака ->
. Если тип возвращаемого значения не Unit
, то в качестве возвращаемого типа принимается последнее (а возможно и единственное) выражение внутри тела лямбды.
Если мы вынесем все необязательные объявления, то, что останется, будет выглядеть следующим образом:
val sum: (Int, Int) -> Int = { x, y -> x + y }
В Kotlin существует соглашение: если последний параметр функции является функцией, то лямбда-выражение, переданное в качестве соответствующего аргумента, может быть заключено в скобки:
val product = items.fold(1) { acc, e -> acc * e }
Такой синтаксис также известен как trailing lambda.
Когда лямбда-выражение является единственным аргументом функции, круглые скобки могут быть опущены:
run { println("...") }
Очень часто лямбда-выражение имеет только один параметр.
Если компилятор способен самостоятельно определить сигнатуру, то объявление параметра можно опустить вместе с ->
.
Параметр будет неявно объявлен под именем it
:
ints.filter { it > 0 } // этот литерал имеет тип '(it: Int) -> Boolean'
Мы можем вернуть значение из лямбды явно, используя оператор return. Либо неявно будет возвращено значение последнего выражения.
Таким образом, два следующих фрагмента равнозначны:
ints.filter {
val shouldFilter = it > 0
shouldFilter
}
ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter
}
Это соглашение, вместе с передачей лямбда-выражения вне скобок, позволяет писать код в стиле LINQ:
strings.filter { it.length == 5 }.sortedBy { it }.map { it.toUpperCase() }
Если параметр лямбды не используется, то разрешено его имя заменить на символ подчёркивания:
map.forEach { _, value -> println("$value!") }
Деструктуризация в лямбдах описана в деструктурирующие объявления.
Единственной особенностью синтаксиса лямбда-выражений, о которой ещё не было сказано, является способность определять и назначать возвращаемый функцией тип. В большинстве случаев в этом нет особой необходимости, потому что он может быть вычислен автоматически. Однако, если у вас есть потребность в определении возвращаемого типа, вы можете воспользоваться альтернативным синтаксисом: анонимной функцией.
fun(x: Int, y: Int): Int = x + y
Объявление анонимной функции выглядит очень похоже на обычное объявление функции, за исключением того, что её имя опущено. Тело такой функции может быть описано и выражением (как показано выше), и блоком:
fun(x: Int, y: Int): Int {
return x + y
}
Параметры функции и возвращаемый тип обозначаются таким же образом, как в обычных функциях. Правда, тип параметра может быть опущен, если его значение следует из контекста:
ints.filter(fun(item) = item > 0)
Аналогично и с типом возвращаемого значения: он вычисляется автоматически для функций-выражений или же должен быть определён вручную (если не является типом Unit
) для анонимных функций, которые имеют в себе блок.
Обратите внимание, что параметры анонимных функций всегда заключены в круглые скобки (...)
. Приём, позволяющий оставлять параметры вне скобок, работает только с лямбда-выражениями.
Одним из отличий лямбда-выражений от анонимных функций является поведение оператора return
(non-local returns). Слово return , не имеющее метки (@
), всегда возвращается из функции, объявленной ключевым словом fun. Это означает, что return внутри лямбда-выражения возвратит выполнение к функции, включающей в себя это лямбда-выражение. Внутри анонимных функций оператор return, в свою очередь, выйдет, собственно, из анонимной функции.
Лямбда-выражение или анонимная функция (так же, как и локальная функция или object expression) имеет доступ к своему замыканию, то есть к переменным, объявленным вне этого выражения или функции. Переменные, захваченные в замыкании, могут быть изменены в лямбде:
var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)
Функциональные типы с получателем, такие как A.(B) -> C
, могут быть вызваны с помощью особой формы - литералов функций с объектом-приёмником.
Как было сказано выше, Kotlin позволяет вызывать экземпляр функционального типа с получателем, предоставляющим объект-приёмник.
Внутри тела литерала объект-приёмник, переданный при вызове функции, становится неявным this, поэтому вы можете получить доступ к членам этого объекта-приёмника без каких-либо дополнительных определителей, а обращение к самому объекту-приёмнику осуществляется с помощью выражения this
.
Это схоже с принципом работы функций-расширений, которые позволяют получить доступ к членам объекта-приёмника внутри тела функции.
Ниже приведён пример литерала с получателем вместе с его типом, где plus
вызывается для объекта-приёмника:
val sum: Int.(Int) -> Int = { other -> plus(other) }
Синтаксис анонимной функции позволяет вам явно указать тип приёмника. Это может быть полезно в случае, если вам нужно объявить переменную типа нашей функции для использования в дальнейшем.
val sum = fun Int.(other: Int): Int = this + other
Лямбда-выражения могут быть использованы как литералы функций с приёмником, когда тип приёмника может быть выведен из контекста. Один из самых важных примеров их использования это типобезопасные строители (type-safe builders):
class HTML {
fun body() { ... }
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML() // создание объекта-приёмника
html.init() // передача приёмника в лямбду
return html
}
html { // лямбда с приёмником начинается тут
body() // вызов метода объекта-приёмника
}