Files
obsidian/Kotlin/Синтаксис и типы/Работа с данными/Деструктуризация.md
T

9.8 KiB
Raw Blame History

title, description, tags
title description tags
Деструктуризация Разбор объекта на переменные, componentN, деструктуризация в лямбдах и циклах
kotlin
syntax

Деструктуризация - разбор объекта на отдельные переменные одной конструкцией.

data class Point(val x: Int, val y: Int)

fun main() {
    val (x, y) = Point(1, 2)
    println("$x $y")      // 1 2
}

Компилятор превращает это в вызовы component1(), component2() и так далее:

data class Point(val x: Int, val y: Int)

fun main() {
    val p = Point(1, 2)
    val x = p.component1()
    val y = p.component2()
    println("$x $y")      // 1 2
}

Что можно деструктурировать

Любой тип, у которого есть функции componentN(). Автоматически они появляются у:

  • data class - по свойствам основного конструктора
  • Pair и Triple
  • Map.Entry - ключ и значение
  • списков и массивов (первые 5 элементов)
fun main() {
    val (a, b) = Pair(1, "x")
    val (c, d, e) = Triple(1, 2, 3)
    val (first, second) = listOf("a", "b", "c")   // третий игнорируется
    println("$a $b | $c $d $e | $first $second")
}

Свой componentN

Для обычного класса операторы объявляются вручную:

class Range2(val from: Int, val to: Int) {
    operator fun component1() = from
    operator fun component2() = to
}

fun main() {
    val (from, to) = Range2(1, 10)
    println("$from..$to")     // 1..10
}

Слово operator обязательно.

Пропуск компонентов

data class User(val id: Int, val name: String, val email: String)

fun main() {
    val (_, name, _) = User(1, "Иван", "i@x.ru")
    println(name)       // Иван
}

Подчёркивание означает «этот компонент не нужен» - componentN() для него даже не вызовется.

Если нужны только первые компоненты, лишние просто не перечисляют:

data class User(val id: Int, val name: String, val email: String)

fun main() {
    val (id, name) = User(1, "Иван", "i@x.ru")   // email не берём
    println("$id $name")                          // 1 Иван
}

В циклах

fun main() {
    val map = mapOf("a" to 1, "b" to 2)
    for ((key, value) in map) {
        println("$key=$value")        // a=1, затем b=2
    }

    val list = listOf("x", "y")
    for ((index, item) in list.withIndex()) {
        println("$index:$item")       // 0:x, затем 1:y
    }
}

Это самое частое применение деструктуризации.

В лямбдах

fun main() {
    val map = mapOf("a" to 1, "b" to 2)
    map.forEach { (k, v) -> println("$k$v") }           // a1 b2
    println(map.map { (k, v) -> "$k=$v" })              // [a=1, b=2]
    map.forEach { (_, v) -> print(v) }                  // 12
    println()

    val points = listOf(1 to 2, 3 to 4)
    println(points.map { (x, y) -> x + y })             // [3, 7]
}

Параметр в скобках { (k, v) -> } - это один параметр, который разбирается, а не два отдельных. Без скобок { k, v -> } лямбда ожидала бы два аргумента, и код не скомпилировался бы.

Возврат нескольких значений

Классическое применение - функция, которая должна вернуть пару значений:

data class Result2(val value: Int, val error: String?)

fun parse(s: String): Result2 =
    s.toIntOrNull()?.let { Result2(it, null) } ?: Result2(0, "не число")

fun main() {
    val (value, error) = parse("42")
    println("$value $error")        // 42 null

    val (v2, e2) = parse("abc")
    println("$v2 $e2")              // 0 не число
}

Pair для этого тоже подходит, но свой data class лучше: имена компонентов осмысленны, а не first/second.

Деструктуризация по именам (Kotlin 2.3+)

Классическая деструктуризация позиционная - порядок переменных должен совпадать с порядком свойств. Это хрупко: перестановка свойств в data class молча меняет смысл кода.

С Kotlin 2.3 разрабатывается деструктуризация по именам. Синтаксис меняется так:

  • val [a, b] = obj - квадратные скобки, позиционно (как было)
  • val (name, email) = obj - круглые скобки, теперь по именам свойств
data class User(val name: String, val email: String)

fun main() {
    val user = User("ИМЯ", "ПОЧТА")

    val (email, name) = user        // по именам — порядок не важен
    println("name=$name email=$email")   // name=ИМЯ email=ПОЧТА

    val [first, second] = user      // позиционно
    println("first=$first")              // first=ИМЯ
}

Фича пока за флагом компилятора:

kotlinc -Xname-based-destructuring=complete App.kt

Значения флага: only-syntax (включить синтаксис [] и полную форму), name-mismatch (предупреждать, когда имена в позиционной деструктуризации не совпадают со свойствами), complete (включить короткую форму по именам).

[!warning] Это ломающее изменение Круглые скобки меняют смысл: был порядок, станут имена. Код val (email, name) = user сегодня присвоит email первое свойство, а после включения фичи - свойство с именем email. Пока флаг не включён, вся деструктуризация в вашем коде позиционная.

[!info] Отличия от Java В Java деструктуризации объектов нет. Ближайший аналог - record patterns из Java 21:

record Point(int x, int y) {}
if (obj instanceof Point(int x, int y)) { ... }

Но это работает только в instanceof и switch, а не при обычном объявлении переменных. Разбор Map.Entry в цикле в Java невозможен - нужно писать e.getKey() и e.getValue().

Java Kotlin
var k = e.getKey(); var v = e.getValue(); val (k, v) = e
for (var e : map.entrySet()) + геттеры for ((k, v) in map)
p.x(); p.y(); val (x, y) = p
нет возврата нескольких значений Pair, Triple, data class

Сравнить: Record в Java

[!warning] Подводные камни Деструктуризация позиционная, а не по именам. Это главная опасность:

data class User(val name: String, val email: String)

fun main() {
    val (name, email) = User("Иван", "i@x.ru")
    println("$name / $email")      // Иван / i@x.ru
}

Если позже в data class поменять порядок свойств на (email, name), этот код продолжит компилироваться, но переменные получат чужие значения. Компилятор не поможет, потому что типы совпадают. Поэтому порядок свойств в data class - часть публичного API.

У обычного класса деструктуризации нет - только у data class или при ручном объявлении componentN().

В лямбде нужны скобки:

fun main() {
    val map = mapOf("a" to 1)
    map.forEach { (k, v) -> println("$k$v") }   // верно: один параметр-пара
    map.forEach { e -> println("${e.key}${e.value}") }   // тоже верно
}

Запись { k, v -> } для forEach на Map не скомпилируется.

Список короче числа переменных - исключение в рантайме:

fun main() {
    val list = listOf(1)
    val (a, b) = list      // компилируется!
    println("$a $b")       // IndexOutOfBoundsException
}

У List есть component1()..component5(), и компилятор не знает фактическую длину. С data class такой проблемы нет.

component1() вызывается даже для неиспользуемой переменной - если это не _. Для свойства с тяжёлым геттером лишняя работа.

Деструктуризация Pair теряет смысл имён. val (a, b) = getCoordinates() не говорит, что где. Лучше data class Coordinates(val lat: Double, val lon: Double).

Далее: Приведение типов