Spec-Zone.ru › Kotlin 1.8

Функции высшего порядка и лямбда-выражения

Функции 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).

Запись со стрелкой является правоассоциативной, (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 } // 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 }
  • Лямбда-выражение всегда окружено фигурными скобками.

  • Объявления параметров в полной синтаксической форме находятся внутри фигурных скобок и имеют необязательные аннотации типов.

  • Тело следует за ->.

  • Если тип возвращаемого значения лямбда-выражения можно вывести, последнее (или, возможно, единственное) выражение внутри тела лямбда-выражения рассматривается как значение возврата.

Если вы опустите все необязательные аннотации, то останется следующее:

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
}
Последнее изменение: 10 января 2023 г.
Функции Встроенные функции

© 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API