219 lines
9.8 KiB
Markdown
219 lines
9.8 KiB
Markdown
---
|
||
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)
|