Files
obsidian/Kotlin/Синтаксис и типы/Работа с данными/Лямбды и функциональные типы.md
T

12 KiB
Raw Blame History

title, description, tags
title description tags
Лямбды и функциональные типы Синтаксис лямбд, it, функциональные типы, ссылки на функции и анонимные функции
kotlin
functional

Функциональный тип описывает функцию как значение: (параметры) -> результат.

val square: (Int) -> Int = { x -> x * x }
val greet: (String) -> String = { "Привет, $it" }
val action: () -> Unit = { println("сделано") }
val sum: (Int, Int) -> Int = { a, b -> a + b }

fun main() {
    println(square(4))        // 16
    println(greet("мир"))     // Привет, мир
    action()                  // сделано
    println(sum(2, 3))        // 5
}

Синтаксис лямбды

Лямбда всегда в фигурных скобках:

{ параметры -> тело }
val f = { x: Int, y: Int -> x + y }    // типы указаны в лямбде
val g: (Int) -> Int = { it * 2 }        // тип указан у переменной

Тип нужно указать либо у переменной, либо у параметров лямбды - где-то компилятор должен его увидеть.

Результат - последнее выражение тела, return не нужен и в обычной лямбде запрещён:

val describe: (Int) -> String = { n ->
    val prefix = if (n > 0) "плюс" else "минус"
    "$prefix $n"                 // это и есть результат
}

fun main() { println(describe(5)) }   // плюс 5

it - неявный параметр

Если параметр один, его можно не объявлять:

val double: (Int) -> Int = { it * 2 }
println(listOf(1, 2, 3).map { it * 10 })     // [10, 20, 30]

it удобен в короткой лямбде. Если тело длинное или лямбды вложены - давайте имя:

val users = listOf("Иван", "Пётр")
println(users.map { name -> name.uppercase() })   // [ИВАН, ПЁТР]

Лямбда последним аргументом

Если последний параметр функции - функция, лямбду выносят за скобки:

fun measure(name: String, block: () -> Unit) {
    val start = System.nanoTime()
    block()
    println("$name занял ${System.nanoTime() - start} нс")
}

fun main() {
    measure("цикл") {              // лямбда вне скобок
        repeat(1000) { }
    }
    measure("пусто", { })          // так тоже можно, но не принято
}

Когда функция принимает только лямбду, круглые скобки опускаются совсем. Именно на этом правиле построены forEach, let, apply и все DSL:

listOf(1, 2).forEach { println(it) }

Подробнее: DSL

Неиспользуемые параметры

val map = mapOf("a" to 1)
map.forEach { (_, value) -> println(value) }    // 1 — ключ не нужен

Подчёркивание говорит компилятору, что параметр намеренно игнорируется.

Ссылки на функции - ::

Вместо лямбды, которая только вызывает существующую функцию, используют ссылку:

fun isEven(n: Int) = n % 2 == 0

fun main() {
    val nums = listOf(1, 2, 3, 4)
    println(nums.filter { isEven(it) })   // [2, 4] — лямбда-обёртка
    println(nums.filter(::isEven))        // [2, 4] — ссылка, короче
    nums.forEach(::println)
}

Виды ссылок:

class User(val name: String) {
    fun greet() = "Привет, $name"
}

fun main() {
    val byName: (User) -> String = User::name       // ссылка на свойство
    val greeter: (User) -> String = User::greet     // на метод класса
    val factory: (String) -> User = ::User          // на конструктор

    val u = User("Иван")
    println(byName(u))                  // Иван
    println(greeter(u))                 // Привет, Иван
    println(factory("Пётр").name)       // Пётр

    val bound = u::greet                // привязанная к объекту
    println(bound())                    // Привет, Иван
}

Анонимные функции

Когда нужен явный return или явный тип результата:

val parse = fun(s: String): Int? {
    if (s.isBlank()) return null       // return работает
    return s.toIntOrNull()
}

fun main() {
    println(parse(" "))      // null
    println(parse("42"))     // 42
}

Отличие от лямбды: return выходит из самой анонимной функции, а в лямбде он запрещён (или выходит из внешней функции, если лямбда inline).

Функциональные типы в подробностях

val a: () -> Unit                       // без параметров, без результата
val b: (Int) -> String                  // один параметр
val c: (Int, Int) -> Int                // два параметра
val d: ((Int) -> Int) -> Int            // принимает функцию
val e: (Int) -> ((Int) -> Int)          // возвращает функцию
val f: String.(Int) -> String           // с получателем (receiver)
val g: ((Int) -> Int)?                  // nullable
val h: suspend () -> Unit               // suspend-функция

Тип с получателем вызывается как метод:

val repeatTimes: String.(Int) -> String = { n -> this.repeat(n) }

fun main() {
    println("ab".repeatTimes(2))     // abab
}

На этом построены apply, buildString и DSL. Подробнее: Receivers и context parameters

Замыкания

Лямбда захватывает переменные из окружения - включая изменяемые:

fun counter(): () -> Int {
    var count = 0            // захвачена по ссылке
    return { ++count }
}

fun main() {
    val next = counter()
    println(next())      // 1
    println(next())      // 2
    println(next())      // 3
}

Это важное отличие от Java, где захватить можно только effectively final переменную.

fun main() {
    var sum = 0
    listOf(1, 2, 3).forEach { sum += it }    // в Java так нельзя
    println(sum)                             // 6
}

Вызов

val f: (Int) -> Int = { it + 1 }

fun main() {
    println(f(1))           // 2 — обычный вызов
    println(f.invoke(1))    // 2 — то же самое
    val g: ((Int) -> Int)? = null
    println(g?.invoke(1))   // null — безопасный вызов nullable-функции
}

Что происходит под капотом

Функциональный тип компилируется в интерфейс FunctionN:

val f: (Int) -> Int = { it * 2 }   // объект класса Function1<Int, Int>

Это значит аллокацию на каждую лямбду, если она захватывает переменные. Для функций высшего порядка в горячем коде существует inline - он подставляет тело лямбды в место вызова, и объекта не создаётся вовсе.

Подробнее: Inline-функции, Kotlin в байт-коде

[!info] Отличия от Java

Java Kotlin
x -> x * 2 { x -> x * 2 } или { it * 2 }
Function<Integer,Integer> (Int) -> Int
BiFunction<A,B,C> (A, B) -> C
Supplier<T> () -> T
Consumer<T> (T) -> Unit
Predicate<T> (T) -> Boolean
Runnable () -> Unit
String::length String::length
нет неявного параметра it
захват только effectively final захват изменяемых переменных
лямбда только для SAM-интерфейса функциональный тип первого класса

В Java лямбда - это реализация функционального интерфейса, поэтому для каждой формы нужен свой тип (Function, BiFunction, Supplier, Consumer, Predicate…). В Kotlin тип функции записывается напрямую: (Int, String) -> Boolean не требует придумывать имя.

Захват изменяемых переменных - практическая разница. Java требует final или effectively final, вынуждая использовать массив-обёртку int[] sum = {0}. В Kotlin var sum = 0 захватывается как есть (компилятор оборачивает его в Ref-объект).

Сравнить: Функции в Java

[!warning] Подводные камни return в лямбде запрещён - он относился бы к внешней функции:

fun find(items: List<Int>): Int? {
    items.forEach {
        if (it > 1) return it     // выход из find, а не из лямбды!
    }                             // работает только потому, что forEach inline
    return null
}

fun main() { println(find(listOf(1, 2, 3))) }   // 2

Это «нелокальный return», он разрешён только в inline-функциях. Для выхода из самой лямбды нужна метка:

fun main() {
    listOf(1, 2, 3).forEach {
        if (it == 2) return@forEach    // пропустить итерацию, как continue
        println(it)                    // 1, затем 3
    }
}

it во вложенных лямбдах затеняется:

fun main() {
    val nested = listOf(listOf(1, 2))
    // println(nested.map { it.map { it * 2 } })   // непонятно, какое it
    println(nested.map { row -> row.map { v -> v * 2 } })   // [[2, 4]]
}

Каждая не-inline лямбда с захватом - объект. В цикле на миллион итераций это миллион аллокаций. Стандартные операции (map, filter, forEach) объявлены inline, поэтому бесплатны; свои функции высшего порядка в горячем коде тоже стоит делать inline.

Лямбда, захватившая this, держит объект в памяти - классическая утечка в Android, когда колбэк переживает активити.

Ссылка на перегруженную функцию неоднозначна:

fun show(x: Int) = "int"
fun show(x: String) = "str"

fun main() {
    // val f = ::show                     // ошибка: какая из двух?
    val f: (Int) -> String = ::show       // тип разрешает неоднозначность
    println(f(1))                          // int
}

Дополнительно: Функции высшего порядка Далее: Деструктуризация