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

274 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Лямбды и функциональные типы
description: Синтаксис лямбд, it, функциональные типы, ссылки на функции и анонимные функции
tags:
- kotlin
- functional
---
Функциональный тип описывает функцию как значение: `(параметры) -> результат`.
```kotlin
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
}
```
## Синтаксис лямбды
Лямбда всегда в фигурных скобках:
```kotlin
{ параметры -> тело }
```
```kotlin
val f = { x: Int, y: Int -> x + y } // типы указаны в лямбде
val g: (Int) -> Int = { it * 2 } // тип указан у переменной
```
Тип нужно указать либо у переменной, либо у параметров лямбды - где-то компилятор должен его увидеть.
Результат - **последнее выражение** тела, `return` не нужен и в обычной лямбде запрещён:
```kotlin
val describe: (Int) -> String = { n ->
val prefix = if (n > 0) "плюс" else "минус"
"$prefix $n" // это и есть результат
}
fun main() { println(describe(5)) } // плюс 5
```
## it - неявный параметр
Если параметр один, его можно не объявлять:
```kotlin
val double: (Int) -> Int = { it * 2 }
println(listOf(1, 2, 3).map { it * 10 }) // [10, 20, 30]
```
`it` удобен в короткой лямбде. Если тело длинное или лямбды вложены - давайте имя:
```kotlin
val users = listOf("Иван", "Пётр")
println(users.map { name -> name.uppercase() }) // [ИВАН, ПЁТР]
```
## Лямбда последним аргументом
Если последний параметр функции - функция, лямбду выносят за скобки:
```kotlin
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:
```kotlin
listOf(1, 2).forEach { println(it) }
```
Подробнее: [DSL](../../Функциональный%20Kotlin/DSL.md)
## Неиспользуемые параметры
```kotlin
val map = mapOf("a" to 1)
map.forEach { (_, value) -> println(value) } // 1 — ключ не нужен
```
Подчёркивание говорит компилятору, что параметр намеренно игнорируется.
## Ссылки на функции - ::
Вместо лямбды, которая только вызывает существующую функцию, используют ссылку:
```kotlin
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)
}
```
Виды ссылок:
```kotlin
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` или явный тип результата:
```kotlin
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).
## Функциональные типы в подробностях
```kotlin
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-функция
```
Тип с получателем вызывается как метод:
```kotlin
val repeatTimes: String.(Int) -> String = { n -> this.repeat(n) }
fun main() {
println("ab".repeatTimes(2)) // abab
}
```
На этом построены `apply`, `buildString` и DSL. Подробнее: [Receivers и context parameters](../../Функциональный%20Kotlin/Receivers%20и%20context%20parameters.md)
## Замыкания
Лямбда захватывает переменные из окружения - **включая изменяемые**:
```kotlin
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 переменную.
```kotlin
fun main() {
var sum = 0
listOf(1, 2, 3).forEach { sum += it } // в Java так нельзя
println(sum) // 6
}
```
## Вызов
```kotlin
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`:
```kotlin
val f: (Int) -> Int = { it * 2 } // объект класса Function1<Int, Int>
```
Это значит аллокацию на каждую лямбду, если она захватывает переменные. Для функций высшего порядка в горячем коде существует `inline` - он подставляет тело лямбды в место вызова, и объекта не создаётся вовсе.
Подробнее: [Inline-функции](../../Функциональный%20Kotlin/Inline-функции.md), [Kotlin в байт-коде](../../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
> [!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](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md)
> [!warning] Подводные камни
> **`return` в лямбде запрещён** - он относился бы к внешней функции:
> ```kotlin
> 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-функциях. Для выхода из самой лямбды нужна метка:
> ```kotlin
> fun main() {
> listOf(1, 2, 3).forEach {
> if (it == 2) return@forEach // пропустить итерацию, как continue
> println(it) // 1, затем 3
> }
> }
> ```
>
> **`it` во вложенных лямбдах затеняется:**
> ```kotlin
> 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, когда колбэк переживает активити.
>
> **Ссылка на перегруженную функцию неоднозначна:**
> ```kotlin
> fun show(x: Int) = "int"
> fun show(x: String) = "str"
>
> fun main() {
> // val f = ::show // ошибка: какая из двух?
> val f: (Int) -> String = ::show // тип разрешает неоднозначность
> println(f(1)) // int
> }
> ```
Дополнительно: [Функции высшего порядка](../../Функциональный%20Kotlin/Функции%20высшего%20порядка.md)
Далее: [Деструктуризация](Деструктуризация.md)