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

219 lines
9.8 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: Разбор объекта на переменные, componentN, деструктуризация в лямбдах и циклах
tags:
- kotlin
- syntax
---
Деструктуризация - разбор объекта на отдельные переменные одной конструкцией.
```kotlin
data class Point(val x: Int, val y: Int)
fun main() {
val (x, y) = Point(1, 2)
println("$x $y") // 1 2
}
```
Компилятор превращает это в вызовы `component1()`, `component2()` и так далее:
```kotlin
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](../Базовые%20типы%20ООП/Data%20class.md) - по свойствам основного конструктора
- `Pair` и `Triple`
- `Map.Entry` - ключ и значение
- списков и массивов (первые 5 элементов)
```kotlin
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
Для обычного класса операторы объявляются вручную:
```kotlin
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` обязательно.
## Пропуск компонентов
```kotlin
data class User(val id: Int, val name: String, val email: String)
fun main() {
val (_, name, _) = User(1, "Иван", "i@x.ru")
println(name) // Иван
}
```
Подчёркивание означает «этот компонент не нужен» - `componentN()` для него даже не вызовется.
Если нужны только первые компоненты, лишние просто не перечисляют:
```kotlin
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 Иван
}
```
## В циклах
```kotlin
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
}
}
```
Это самое частое применение деструктуризации.
## В лямбдах
```kotlin
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 -> }` лямбда ожидала бы два аргумента, и код не скомпилировался бы.
## Возврат нескольких значений
Классическое применение - функция, которая должна вернуть пару значений:
```kotlin
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` - **круглые** скобки, теперь **по именам** свойств
```kotlin
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=ИМЯ
}
```
Фича пока за флагом компилятора:
```bash
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:
> ```java
> 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](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Record.md)
> [!warning] Подводные камни
> **Деструктуризация позиционная, а не по именам.** Это главная опасность:
> ```kotlin
> 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()`.
>
> **В лямбде нужны скобки:**
> ```kotlin
> 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` не скомпилируется.
>
> **Список короче числа переменных - исключение в рантайме:**
> ```kotlin
> 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)`.
Далее: [Приведение типов](Приведение%20типов.md)