Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user