12 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Лямбды и функциональные типы | Синтаксис лямбд, it, функциональные типы, ссылки на функции и анонимные функции |
|
Функциональный тип описывает функцию как значение: (параметры) -> результат.
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) -> IntBiFunction<A,B,C>(A, B) -> CSupplier<T>() -> TConsumer<T>(T) -> UnitPredicate<T>(T) -> BooleanRunnable() -> UnitString::lengthString::lengthнет неявного параметра itзахват только effectively final захват изменяемых переменных лямбда только для SAM-интерфейса функциональный тип первого класса В Java лямбда - это реализация функционального интерфейса, поэтому для каждой формы нужен свой тип (
Function,BiFunction,Supplier,Consumer,Predicate…). В Kotlin тип функции записывается напрямую:(Int, String) -> Booleanне требует придумывать имя.Захват изменяемых переменных - практическая разница. Java требует
finalили effectively final, вынуждая использовать массив-обёрткуint[] sum = {0}. В Kotlinvar 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 }
Дополнительно: Функции высшего порядка Далее: Деструктуризация