Функции высшего порядка и лямбда-выражения
Функции 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. Эти имена можно использовать для документирования значения параметров.
Чтобы указать, что тип функции может иметь значение null, используйте скобки следующим образом: ((Int, Int) -> Int)?.
Типы функций также могут быть объединены с помощью скобок: (Int) -> ((Int) -> Unit).
Также можно присвоить типу функции альтернативное имя с помощью псевдонима типа:
typealias ClickHandler = (Button, ClickEvent) -> Unit
Создание экземпляров типа функции
Существует несколько способов получения экземпляра типа функции:
-
Используйте блок кода внутри лямбда-выражения в одном из следующих форматов:
a лямбда-выражение:
{ 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
}
Встроенные функции
Иногда целесообразно использовать встроенные функции, которые обеспечивают гибкий контроль потока, для функций высшего порядка.
Лямбда-выражения и анонимные функции
Лямбда-выражения и анонимные функции являются литералами функций. Литералы функций — это функции, которые не объявляются, а передаются сразу как выражение. Рассмотрим следующий пример:
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–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/lambdas.html