Мульти-декларации

Иногда удобно деструктуризировать объект на несколько переменных, например:

val (name, age) = person

Этот синтаксис называется деструктуризирующим присваиванием. Он позволяет присвоить объект сразу нескольким переменным, разбив его на части. Вы объявили две переменные: name и age, и теперь можете использовать их по отдельности.

println(name)
println(age)

Эта декларация транслируется в такой код:

val name = person.component1()
val age = person.component2()

Функции component1() и component2() — это ещё один пример принципа конвенций, широко используемого в Kotlin (например, для операторов + и * или циклов for). В правой части деструктурирующей декларации может находиться что угодно, если для этого объекта можно вызвать нужное количество функций componentN(). Конечно, это могут быть component3(), component4() и так далее.

Функции componentN() нужно отмечать ключевым словом operator, чтобы позволить их использование в деструктуризирующем присваивании.

Деструктуризирующие присваивания также работают в циклах for:

for ((a, b) in collection) { /* ... */ }

В данном примере переменные a и b получают значения, возвращённые методами component1() и component2(), неявно вызванными для элементов коллекции.

Пример: возврат двух значений из функции

Предположим, вам нужно вернуть два значения из функции. Например, результат вычисления и какой-нибудь статус. Компактный способ достичь этого — объявление data-класса и возвращение его экземпляра.

data class Result(val result: Int, val status: Status)
fun function(...): Result {
    // вычисления

    return Result(result, status)
}

// Теперь вы можете использовать деструктуризирующее присваивание:
val (result, status) = function(...)

Так как data-классы автоматически объявляют componentN()-функции, мульти-декларации будут работать с ними “из коробки”.

Вы также могли использовать стандартный класс Pair, чтобы заставить функцию вернуть Pair<Int, Status>, но правильнее будет именовать ваши данные должным образом.

Пример: мульти-декларации и ассоциативные списки

Пожалуй, самый хороший способ итерации по ассоциативному списку:

for ((key, value) in map) {
   // do something with the key and the value
}

Чтобы это работало, вы должны:

  • представить ассоциативный список как последовательность значений, предоставив функцию iterator(),
  • представить каждый элемент как пару с помощью функций component1() и component2().

И да, стандартная библиотека предоставляет такие расширения:

operator fun <K, V> Map<K, V>.iterator(): Iterator<Map.Entry<K, V>> = entrySet().iterator()
operator fun <K, V> Map.Entry<K, V>.component1() = getKey()
operator fun <K, V> Map.Entry<K, V>.component2() = getValue()

Так что вы можете свободно использовать мульти-декларации в циклах for с ассоциативными списками (так же как и с коллекциями экземпляров data-классов).

Подчеркивание для неиспользуемых переменных

Если вам не нужна переменная в объявлении деструктурирования, вы можете поместить символ подчеркивания вместо ее имени.

val (_, status) = getResult()

Операторные функции componentN() не вызываются для компонентов, которые пропускаются таким образом.

Деструктурирование в лямбдах

Вы можете использовать синтаксис объявлений деструктурирования для лямбда-параметров. Если у лямбды есть параметр типа Pair (или Map.Entry, или любого другого типа, который имеет соответствующие функции componentN), вы можете ввести несколько новых параметров вместо одного, заключив их в круглые скобки.

map.mapValues { entry -> "${entry.value}!" }
map.mapValues { (key, value) -> "$value!" }

Обратите внимание на разницу между объявлением двух параметров и объявлением пары деструктурирования вместо параметра.

{ a -> ... } // один параметр
{ a, b -> ... } // два параметра
{ (a, b) -> ... } // пара деструктурирования
{ (a, b), c -> ... } // пара деструктурирования и параметр

Если компонент деструктурированного параметра не используется, вы можете заменить его символом подчеркивания, чтобы избежать выдумывания его имени.

map.mapValues { (_, value) -> "$value!" }

Вы можете указать тип для всего деструктурированного параметра или отдельно для конкретного компонента.

map.mapValues { (_, value): Map.Entry<Int, String> -> "$value!" }

map.mapValues { (_, value: String) -> "$value!" }

Деструктурирование по именам

Kotlin поддерживает деструктурирующие декларации по именам, в которых переменные сопоставляются со свойствами по имени, а не по позиции, заданной функциями componentN() при позиционном деструктурировании.

Подробнее о деструктурировании по именам см. в KEEP этой возможности.

При позиционном деструктурировании переменные соответствуют порядку функций componentN(), например:

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

fun main() {
    val user = User("alice", "[email protected]")

    val (email, username) = user

    println(email)
    // alice

    println(username)
    // [email protected]
}

В этом примере деструктурирование опирается на порядок функций componentN(), поэтому email получает значение username, а username получает значение email.

При деструктурировании по именам извлекаемые значения определяются именами свойств, а не позициями функций componentN():

fun main() {
    val user = User("alice", "[email protected]")

    // Используется явная форма деструктурирования по именам
    (val mail = email, val name = username) = user

    println(name)
    // alice

    println(mail)
    // [email protected]
}

Деструктурирование по именам — экспериментальная возможность. Когда вы включаете её, появляется и новый синтаксис для позиционного деструктурирования с использованием квадратных скобок. Используйте этот синтаксис для типов, где важен порядок элементов, например для списков и других упорядоченных коллекций, а также для неименованных кортежей вроде Pair или Triple:

val point = Pair(10, 20)

// Используется позиционное деструктурирование
val [x, y] = point

Вы можете управлять тем, как компилятор интерпретирует деструктурирующие декларации, с помощью параметра компилятора -Xname-based-destructuring.

У него есть следующие режимы:

  • only-syntax включает явную форму деструктурирования по именам, не меняя поведение существующих деструктурирующих деклараций.
  • name-mismatch сообщает предупреждения, когда при позиционном деструктурировании data-классов используются имена переменных, не совпадающие с именами свойств.
  • complete включает короткую форму деструктурирования по именам с круглыми скобками и продолжает поддерживать позиционное деструктурирование с синтаксисом квадратных скобок.

Прежде чем включать режим complete, просмотрите и исправьте предупреждения, полученные в режиме name-mismatch. Эти предупреждения показывают, какие деструктурирующие декларации компилятор будет интерпретировать иначе в режиме complete, и содержат предложения по их переписыванию.

Если вы используете режим complete, короткий синтаксис деструктурирования с круглыми скобками сопоставляет переменные с именами свойств, а не опирается на позицию:

val (email, username) = user

Чтобы включить деструктурирование по именам в проекте, добавьте параметр компилятора в файл конфигурации сборки:

Gradle

kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xname-based-destructuring=only-syntax")
    }
}

Maven

<build>
    <plugins>
        <plugin>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-plugin</artifactId>
            <configuration>
                <args>
                    <arg>-Xname-based-destructuring=only-syntax</arg>
                </args>
            </configuration>
        </plugin>
    </plugins>
</build>