Функции высшего порядка и лямбда-выражения
Функции Kotlin являются первоклассными, что означает, что они могут храниться в переменных и структурах данных, а также передаваться в качестве аргументов и возвращаться другими функциями высшего порядка. Вы можете выполнять над функциями любые операции, возможные для других значений, не являющихся функциями.
Для этого Kotlin, как статически типизированный язык программирования, использует семейство типов функций для представления функций и предоставляет набор специализированных конструкций языка, таких как лямбда-выражения.
Функции высшего порядка
Функция высшего порядка — это функция, которая принимает функции в качестве параметров или возвращает функцию.
Хороший пример функции высшего порядка — идиома функционального программирования fold для коллекций. Она принимает начальное значение аккумулятора и функцию комбинирования и строит своё возвращаемое значение, последовательно комбинируя текущее значение аккумулятора с каждым элементом коллекции, заменяя значение аккумулятора каждый раз:
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() {
//sampleStart
val items = listOf(1, 2, 3, 4, 5)
// Lambdas are code blocks enclosed in curly braces.
items.fold(0, {
// When a lambda has parameters, they go first, followed by '->'
acc: Int, i: Int ->
print("acc = $acc, i = $i, ")
val result = acc + i
println("result = $result")
// The last expression in a lambda is considered the return value:
result
})
// Parameter types in a lambda are optional if they can be inferred:
val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i })
// Function references can also be used for higher-order function calls:
val product = items.fold(1, Int::times)
//sampleEnd
println("joinedToString = $joinedToString")
println("product = $product")
}
Типы функций
Kotlin использует типы функций, такие как (Int) -> String, для объявлений, связанных с функциями: val onClick: () -> Unit = ....
Эти типы имеют специальную запись, соответствующую сигнатурам функций — их параметрам и возвращаемым значениям:
Все типы функций имеют заключенный в скобки список типов параметров и тип возвращаемого значения:
(A, B) -> Cобозначает тип, представляющий функции, которые принимают два аргумента типовAиBи возвращают значение типаC. Список типов параметров может быть пустым, как в() -> A. ТипUnitвозвращаемого значения нельзя опустить.Типы функций могут необязательно иметь дополнительный тип получателя, который указывается перед точкой в записи: тип
A.(B) -> Cпредставляет функции, которые могут вызываться на объекте получателяAс параметромBи возвращать значениеC. Лямбда-выражения с получателем часто используются вместе с такими типами.Подвешенные функции относятся к специальному виду типов функций, у которых есть модификатор suspend в их записи, такой как
suspend () -> Unitилиsuspend A.(B) -> C.
Запись типа функции может необязательно включать имена параметров функции: (x: Int, y: Int) -> Point. Эти имена можно использовать для документирования смысла параметров.
Для указания того, что тип функции является нулевым, используйте скобки следующим образом: ((Int, Int) -> Int)?.
Типы функций также могут комбинироваться, используя скобки: (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 } // The inferred type is (Int) -> Int
Нелитеральные значения типов функций с и без получателя взаимозаменяемы, поэтому получатель может выступать в качестве первого параметра, и наоборот. Например, значение типа (A, B) -> C может передаваться или присваиваться там, где ожидается значение типа A.(B) -> C, и наоборот:
fun main() {
//sampleStart
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
//sampleEnd
println("result = $result")
}
Вызов экземпляра типа функции
Значение типа функции можно вызвать, используя его invoke(...) оператор: f.invoke(x) или просто f(x).
Если значение имеет тип получателя, объект получателя должен передаваться как первый аргумент. Другой способ вызвать значение типа функции с получателем — это поместить объект получателя перед ним, как если бы значение было расширяющей функцией: 1.foo(2).
Пример:
fun main() {
//sampleStart
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)) // extension-like call
//sampleEnd
}
Встраиваемые функции
Иногда полезно использовать встраиваемые функции, которые обеспечивают гибкий контроль потока для функций высшего порядка.
Lambda-выражения и анонимные функции
Lambda-выражения и анонимные функции являются литералами функций. Литералы функций — это функции, которые не объявляются, а сразу передаются как выражение. Рассмотрим следующий пример:
max(strings, { a, b -> a.length < b.length })
Функция max является функцией высшего порядка, так как она принимает значение функции в качестве второго аргумента. Этот второй аргумент — выражение, которое само по себе является функцией, называемой литералом функции, что эквивалентно следующей именованной функции:
fun compare(a: String, b: String): Boolean = a.length < b.length
Синтаксис лямбда-выражений
Полный синтаксис лямбда-выражений выглядит следующим образом:
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
Лямбда-выражение всегда заключено в фигурные скобки.
Объявления параметров в полном синтаксисе находятся внутри фигурных скобок и могут иметь необязательные аннотации типов.
Тело следует за
->.Если выведенный тип возвращаемого значения лямбда-выражения не
Unit, последнее (или возможно единственное) выражение внутри тела лямбда-выражения рассматривается как значение возврата.
Если вы опустите все необязательные аннотации, то останется следующее:
val sum = { x: Int, y: Int -> x + y }
Передача хвостовых лямбда-выражений
В соответствии с соглашениями Kotlin, если последний параметр функции является функцией, то лямбда-выражение, переданное в качестве соответствующего аргумента, может быть помещено за пределами скобок:
val product = items.fold(1) { acc, e -> acc * e }
Такой синтаксис также известен как хвостовое лямбда-выражение.
Если лямбда-выражение является единственным аргументом в этом вызове, скобки можно опустить полностью:
run { println("...") }
it: неявное имя единственного параметра
Очень часто лямбда-выражение имеет только один параметр.
Если компилятор может проанализировать сигнатуру без параметров, то параметр не нужно объявлять, и -> можно опустить. Параметр будет неявным образом объявлен под именем it:
ints.filter { it > 0 } // this literal is of type '(it: Int) -> Boolean'
Возвращение значения из лямбда-выражения
Вы можете явно вернуть значение из лямбда-выражения, используя синтаксис квалифицированного возврата. В противном случае, значение последнего выражения неявно возвращается.
Следовательно, два следующих фрагмента эквивалентны:
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.uppercase() }
Подчеркивание для неиспользуемых переменных
Если параметр лямбда-выражения не используется, вы можете поместить подчеркивание вместо его имени:
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 без метки всегда возвращает из функции, объявленной с ключевым словом fun. Это означает, что return внутри лямбда-выражения вернет управление из окружающей функции, а return внутри анонимной функции вернет управление из самой анонимной функции.
Замыкания
Лямбда-выражение или анонимная функция (а также локальная функция и объектное выражение) может получить доступ к своему замыканию, которое включает в себя переменные, объявленные во внешней области видимости. Переменные, захваченные в замыкании, могут быть изменены в лямбда-выражении:
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
Лямбда-выражения могут использоваться в качестве литералов функций с получателем, когда тип получателя может быть определен из контекста. Одним из самых важных примеров их использования являются безопасные по типу билдеры:
class HTML {
fun body() { ... }
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML() // create the receiver object
html.init() // pass the receiver object to the lambda
return html
}
html { // lambda with receiver begins here
body() // calling a method on the receiver object
}
© 2010–2022 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/lambdas.html