Объявления деструктуризации в Kotlin

04 октября 2021

Объявления деструктуризации - это особенность Kotlin, которая дает вам инструмент для легкого извлечения данных из коллекции простым и доступным способом.  

В процессе программирования на Kotlin, вы работаете с большим количеством данных. Иногда вы собираете данные в коллекцию, такую как массив, пару, класс данных или класс. И, конечно, вам нужен способ, как извлечь данные из этих коллекций. Это и есть Объявление Деструктуризации, о которой вы узнаете в этом туториале.

Есть много способов извлечь данные из коллекции, связанных с использованием индекса. С Объявлениями Деструктуризации Kotlin дает вам инструмент для легкого извлечения данных из коллекции, независимо от ее размера.

В этом туториале вы создадите Destructuring Cryptocurrencies - простое консольное приложение, делающее акцент на применение объявлений деструктуризации. Пока вы будете изучать названия различных типов криптовалют, вы узнаете об:

  • Объявлении деструктуризации пары, массива и карты.
  • Варианте использования объявлений деструктуризации в Лямбда.
  • Проблеме с объявлениями деструктуризации класса данных.
  • Как создать объявления деструктуризации поддерживаемые классом.

Начнем

Загрузите стартовый проект нажав наСкачать Материалы. Откройте его в IntelliJ IDEA. 

Откройте DestructuringCryptocurrencies.kt. В этом файле вы увидите некоторые данные, включая пару, массив и класс данных. Далее с ними вы будете использовать объявления деструктуризации. 

Объявления деструктуризации

Во-первых, вам может быть интересно, что означает термин Объявление Деструктуризации. Простыми словами, это процесс создания нескольких переменных в одной строке кода. Это синтаксическая особенность Kotlin, которая возможно будет непонятна вначале, но может пригодиться и помочь сделать ваш код чище.  

Обычно она используется в коллекциях, но так же и в других типах данных. В этом туториале вы узнаете, как использовать их в: 

  • Массивах
  • Парах
  • Картах
  • Лямбдах
  • Классах данных

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

Объявления Деструктуризации пары, массива и карты

Первое и наиболее распространенное использование объявлений деструктуризации - это структуры данных. Здесь вы будете использовать их с парой, массивом и картой. Это общие типы данных, из которых вы можете извлечь элементы.

Объявления Деструктуризации пары

Сначала начните с пары, которая, как вы, возможно, знаете, представляет собой структуру данных, содержащую два элемента.

Чтобы начать Скомпилируйте и Запустите образец проекта. Поскольку это консольное приложение, вы можете проверить вывод приложения в окне панели запуска. Вы получите информацию:

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

Сначала вы воспользуетесь объявлением деструктуризации для пары. При извлечении информации из пары вы можете извлекать элементы по одному, используя свойства first и second, что является наиболее распространенным способом. Но с объявлениями деструктуризации вы можете извлечь оба элемента из пары за один раз.

Замените TODO1 на:

val (stringFromPair1, stringFromPair2) = pair
println("$stringFromPair1, $stringFromPair2")

Скомпилируйте и запустите. Вы можете увидеть значение stringFromPair1 и stringFromPair2.

Объявления деструктуризации аналогично объявлению обычной переменной. Вы выбираете, хотите ли вы использовать val или var. Далее вы заключаете две переменные, разделенные запятыми, в круглые скобки. 

Код выглядит намного проще, чем предыдущий пример извлечения. 

Вы должны использовать val или var для обеих переменных. Вы не можете использовать val для одной переменной и var для другой переменной. В том случае, когда вам нужно использовать val для одного значения и var для другого значения, вы не можете использовать объявления деструктуризации.

Если вы не используете объявления деструктуризации, вам нужно будет извлекать значения одно за другим с помощью оператора индекса. Использование объявлений деструктуризации позволяет сэкономить несколько лишних строк.

Однако объявления деструктуризации не только сохраняют строки, но и в некоторых случаях более естественны. Например, рассмотрим пару как коллекцию данных координат в 2D-ландшафте. Вы можете извлечь x и y по одному из пары следующим образом:

val x = coordinates[0]
val y = coordinates[1]

Но более естественно использовать объявления деструктуризации, чтобы извлечь их как пару, как здесь:

val (x, y) = coordinates

Это легче прочесть, потому что вам не нужно читать индекс 0 и индекс 1, которые являются лишними. 

Объявления деструктуризации массива

Теперь давайте продолжим с более распространенной структурой данных, которая содержит не просто два значения, а множество. В следующем примере вы будете использовать объявления деструктуризации для массива.

Для начала обратите внимание на ограничения деструктуризации массива и других способов извлечения элементов из него. В коде вы видите два примера того, как получить элемент из массива; теперь представьте, что вам нужно повторить это для каждого элемента. Взгляните на приведенный ниже код, чтобы понять, о чем идет речь:

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

Если вы раскомментируете строку в приведенном выше примере кода, то заметите ошибку в шестом элементе. Теперь строка будет выглядеть следующим образом:

val (string1, string2, string3, string4, string5, string6) = stringList

Скомпилируйте и запустите. Как видите, вы получаете ошибку. 

Ошибка:  отсутствует component6.

Теперь отмените изменения. Закомментируйте строку снова, чтобы она стала:

// val (string1, string2, string3, string4, string5, string6) = stringList

Скомпилируйте и запустите снова. Приложение должно вернуться в нормальное состояние. 

Отличная работа! Вы уже узнали, как выполнять Объявления Деструктуризации с двумя типами данных: парами и массивами. Продолжайте изучать и узнайте о еще больших возможностях.

Component1, Component2, …, ComponentN

В предыдущем примере ошибка говорит нам о том, что мы можем использовать подобные методы с индексами ниже того, на который она указывает, наподобие component5. Выполнение val componentString5 = stringList.component5() - это один из способов извлечь элемент из массива. Как вы уже догадались, вы получите пятый элемент с component5. Но, как упоминалось ранее, вам нужно будет повторить эту строку несколько раз, чтобы извлечь все компоненты.

Тем не менее, дизайнеры Kotlin полагали, что разработчикам не потребуется больше, чем пять элементов, при использовании объявлений деструктуризации в массиве. Это ограничение присутствует в похожих типах данных, например списках.

Далее вы увидите, как добавить в класс способ для извлечения более пяти элементов в объявлениях деструктуризации.

Следующая вещь, которую стоит отметить, - вы получите похожую ошибку, если раскомментируете эту строку val string6 = stringList.component6().

component1, component2componentN,  где N  - это наибольший элемент, который вам нужно извлечь, - являются ядром объявлений деструктуризации. 

Объявления деструктуризации - это так же одна из особенностей языка Kotlin, которые обычно называютсинтаксический сахардля доступа к component1, component2, …, componentN  в объекте. 

Массив или список поддерживают извлечение только до пяти элементов, потому что их классы определяют компоненты только с 1 по 5. Вы можете увидеть определение component5 в этой документации Kotlin, но вы не найдете определение component6.

Помимо использования такого метода, как component5, вы также можете извлечь элемент с помощью обычного метода, такого как индекс, например stringList [3].

Можете ли вы предположить, почему дизайнеры Kotlin сделали возможным извлечь с помощью метода объявления деструктуризации максимум 5 элементов? Если бы не было установлено ограничение, вы бы могли извлечь десять элементов, как здесь:

val (tree, mountain, bridge, beach, castle, river, waterfall, forest, cave, tower, building, fortress) = words

Строка выше имеет более ста символов в ширину. Нехорошо писать слишком длинный код, потому что его сложнее читать. Вы можете разделить переменные с помощью новых строк, но в таком случае вы также можете использовать и оператор индекса для извлечения значений.

Более того, вызвать несколько элементов уже сложнее. В примере наверху cave - девятый элемент в words. Чтобы убедиться, что элемент cave получил верное значение, вы должны посчитать от первого элемента tree до девятого элемента cave и при этом не ошибиться в подсчете. 

Вы так же можете использовать оператор индекса как здесь:

val tree = words[0]
val cave = words[8]

Имея в виду все это, дизайнеры Kotlin установили максимальное количество элементов равным пяти при использовании объявлений деструктуризации со структурой данных типа коллекции. Если вы извлекаете более пяти элементов, вы злоупотребляете API.

Но что, если вам нужны только первый и пятый элементы, но не три элемента между ними? Как это сделать вы узнаете дальше.

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

Вы можете опустить переменные, которые вам не нужны, с помощью подчеркивания. Используя этот подход, вы избегаете загрязнения места для имен. За кадром Kotlin не будет вызывать component2, component3 и component4.

Замените TODO 2 кодом ниже:

val (string1, _, _, _, string5) = stringList
println("$string1, $string5")

Скомпилируйте и запустите. Вы можете увидеть значения string1 и string5.

 

 В этом случае Python вдохновил Kotlin. В Python подчеркивание - это выброшенная переменная, значение которой вы хотите проигнорировать.

Если вам интересно, почему вы не можете использовать разные имена для этих выброшенных переменных, ответ - вы можете. Но вам нужно будет придумать разные названия для более чем одной игнорируемой переменной. Для примера, вы можете использовать для именования переменных всего один символ, как здесь:

val (string1, a, b, c, string5) = stringList
println("$string1, $string5")

В приведенном выше коде вы игнорируете a, b и c. Но вы как-то должны хранить лишнюю информацию о том, что есть три переменные, которые в конечном итоге вы не используете в остальной части кода. Для вашего мозга легче использовать подчеркивание. 

Но что, если вы хотите использовать одно и то же имя для каждой неиспользуемой переменной? Например, выбираем tmp в качестве имени для каждой такой переменной как здесь:

val (string1, tmp, tmp, tmp, string5) = stringList
println("$string1, $string5")

Если вы используете одно и то же имя для нескольких объявлений переменных, вы получите ошибку: конфликт объявлений.

Отличная работа! Теперь, когда вы знаете, как использовать объявления деструктуризации, вы изучите распространенные варианты применения, в которых вы захотите их использовать.

Общий сценарий использования Объявлений Деструктуризации

И хотя объявления деструктуризации не так часты в коде новичка, они могут быть полезны во многих ситуациях и сделать ваш код лучше. В этой части мы пройдемся по распространенным случаям их применения. 

Замените TODO 3 на следующий код:

val (bitcoinFromFunction, btcFromFunction) = functionReturningPair()
println("$bitcoinFromFunction, $btcFromFunction")

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

fun functionReturningPair() : Pair = Pair("Bitcoin", “BTC”)

Довольно простая функция, вы просто возвращаете пару. В реальной жизни такая функция будет выполнять определенную обработку, а затем возвращать значение, но для примера она упрощена. 

Скомпилируйте и запустите. Вы видите значения bitcoinFromFunction и btcFromFunction.

Разработчики обычно используют объявления деструктуризации, когда они хотят вернуть более одного значения в функции и использовать их сразу после выполнения функции.

Альтернативой этому будет объединение результата возврата со структурой данных коллекции, а затем разделение его с помощью оператора индекса или объявлений деструктуризации. Как показано в приведенном ниже коде, это неэффективно:

val aPair = functionReturningPair()
val bitcoinFromFunction = aPair[0]
val btcFromFunction = aPair[1]
println("$bitcoinFromFunction, $btcFromFunction")

Код болеемногословный”. 

Достаточно обычно вернуть из функции коллекцию данных. Например, у вас может быть функция, которая вычисляет долготу и широту местоположения. 

Но за пределами функции возможно вы захотите использовать широту и долготу отдельно друг от друга, а не в связке. В этой ситуации удобно использовать объявления деструктуризации. 

Объявления деструктуризации карты

Другая структура данных, в которой вы можете использовать объявления деструктуризации, - это итерация карты. Карты - это пары ключ-значение, сгруппированные в коллекцию. Они похожи на массив, с той разницей, что вы можете дать ключу имя, а не порядковый номер. 

Замените TODO 4 на: 

for ((cryptoFullname, cryptoSymbol) in cryptoSymbolMap) {
    println("$cryptoFullname, $cryptoSymbol")
}

Скомпилируйте и запустите. Вы можете увидеть ключ и значение каждого элемента в карте. 

В коде выше cryptoSymbolMap является картой. Вы используете объявление деструктуризации элемента в то время, как происходит итерация карты, заключая две разделенные запятой переменные в скобки. 

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

Таким же способом вы производите итерацию карты с методом forEach.

Объявление деструктуризации в лямбда-функциях.

Лямбда-функции очень распространены в коде Kotlin и в большинстве современных языков программирования, лямбда-функции имеют синтаксис {a, b -> a + b}. В этом разделе мы рассмотрим, как объявления деструктуризации могут быть полезны в такого рода функциях.

Замените TODO 5 на: 

cryptoSymbolMap.forEach { (cryptoFullname, cryptoSymbol) ->
    println("$cryptoFullname, $cryptoSymbol")
}

Скомпилируйте и запустите. Вы увидите ключ и значение каждого элемента в карте.

 

В приведенном выше коде forEach принимает лямбда, принимающая параметр. Вы используете объявления деструктуризации для этого параметра, чтобы извлечь их в две переменные.

Тип переменной в результате каждой итерации - Entry, представляющий пару ключ/значение внутри Map. В нем есть методы, с которыми вы знакомы, например: component1 и component2.

Если вы прочитаете определение component1, вы увидите, что метод возвращает компонент Ключа этого Entry. Точно так же, если вы прочитаете определение component2, вы увидите, что метод возвращает компонент Значения этого Entry. Также существует метод преобразования Entry в пару. 

Объявления деструктуризации в лямбду

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

Посмотрите на пример лямбды с двумя параметрами:

val lambdaCrypto = { name: String, symbol: String ->
    println("$name, $symbol")
}
lambdaCrypto("Bitcoin", "BTC")

Здесь вы вызываете лямбду с двумя параметрами: именем и символом, которые имеют тип String.

Теперь посмотрим на пример той же лямбды только с одним параметром. Вы используете объявления деструктуризации для этого параметра, чтобы извлечь их в две переменные.

Замените TODO 6 на: 

val lambdaCrypto2 = { (name: String, symbol: String): Parameter ->
    println("$name, $symbol")
}
lambdaCrypto2(param)

Скомпилируйте и запустите. Вы можете увидеть значения name и symbol.

 

Обратите внимание на код выше, две переменные заключены в круглые скобки, в то время как в лямбде, принимающей две переменные, скобок нет. 

Если вы пришли из Python, вы можете задаться вопросом, можете ли вы деструктурировать или распаковать аргументы при вызове функции, вместо того, чтобы делать это внутри функции как здесь:

lambdaCrypto(*Pair("Bitcoin", "BTC"))

В Kotlin есть оператор Spread. Однако он не такой гибкий, как в Python. Из-за этого вы получите ошибку. Вы можете применить оператор Spread только в позиции vararg.

Проблема с деструктуризацией класса данных

Один из наиболее распространенных типов данных, который вы будете извлекать с помощью объявлений деструктуризации, - это класс данных. Однако вы должны быть осторожны, потому что объявления деструктуризации класса данных основаны на позициях, а не на именах.

Посмотрите на этот экземпляр класса данных и пример неправильного использования объявлений деструктуризации:

data class Founder(var name: String, var cryptocurrency: String, var country: String)
val founder = Founder("Vitalik", "Ethereum", "Canada")
// val (cryptocurrency, name, country) = founder

cryptocurrency имеет значение Vitalik, не  Ethereum.

Чтобы доказать, что это так, раскомментируйте:

// val (cryptocurrency, name, country) = founder

Затем добавьте println, чтобы вывести значение. Строка будет выглядеть так:

val (cryptocurrency, name, country) = founder
println(cryptocurrency)

Скомпилируйте и запустите.  И вы увидите значение cryptocurrency.

Как видите, cryptocurrency действительно имеет значение Vitalik.

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

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

// val (cryptocurrency, name, country) = founder

Далее замените  TODO 7 на:

val (name, cryptocurrency, country) = founder
println("$name, $cryptocurrency, $country")

Скомпилируйте и запустите. Вы увидите значения name, cryptocurrency и country.

 

Теперь name, cryptocurrency и country относятся к правильным значениям.

Если вас раздражает не более пяти элементов в объявлении деструктуризации коллекции, то у меня для вас отличные новости! У класса данных нет этого ограничения.

Даже если ваш класс данных состоит из двадцати компонентов, вы можете извлечь их все с помощью объявлений деструктуризации.

Однако вы не можете предоставить явную реализацию для componentN в классе данных. Итак, если вы хотите разыграть своих коллег, переопределив component2, чтобы вернуть третий элемент, и component3, чтобы вернуть первый элемент, вы можете забыть об этом. К счастью, дизайнеры Kotlin наложили ограничения, чтобы защитить ваших коллег от ваших жестоких шуточек. :]

Но что, если вы упорствуете в своих попытках разыграть своих коллег? Здесь вам поможет специальный класс.

Создание поддержки объявлений деструктуризации для класса

Что если вы хотите создать поддержку объявлений деструктуризации более пяти элементов для класса? Взгляните на определение Cryptocurrencies:

class Cryptocurrencies {
    operator fun component1() : String = "Bitcoin"
    operator fun component2() : String = "Ethereum"
    operator fun component3() : String = "Binance Coin"
    operator fun component4() : String = "Tether"
    operator fun component5() : String = "XRP"
    operator fun component6() : String = "Uniswap"
}

Помните component5, который вы использовали для извлечения пятого элемента из массива? Если вы хотите извлечь шестой элемент из класса, определите component6.

Метод должен быть операторным. Если вы хотите извлечь двадцать элементов из класса, вам нужно определить их от component1 до component20.

Замените TODO 8  на:

val (bitcoin, ethereum, binanceCoin, tether, xrp, uniswap) = Cryptocurrencies()
println("$bitcoin, $ethereum, $binanceCoin, $tether, $xrp, $uniswap")

Скомпилируйте и запустите. 

Как видите, вы извлекли шесть элементов из экземпляра класса.

Помните, вы хотели разыграть своих коллег? Вы можете переопределить component3, чтобы вернуть что-то раздражающее. Например, вы можете сделать что-то вроде этого:

//operator fun component3() : String = "Binance Coin"
  operator fun component3() : String {
      val current = LocalDateTime.now()
      if (current.monthValue % 2 == 0) {
          return "Binance Coin"
      } else {
          return "Doge"
      }
  }

Если ваши коллеги используют этот класс, они получат разные данные при извлечении component3 в зависимости от того, четный или нечетный текущий месяц. Не забудьте сначала импортировать java.time.LocalDateTime, если вы действительно собрались их разыграть. 

В этом уроке вы узнали:

  • Что такое объявления деструктуризации и как их использовать.
  • Как использовать объявления деструктуризации в коллекциях: Пары, Массивы и Карты.
  • Объявления деструктуризации в лямбда-функциях.
  • Использование объявлений деструктуризации в классах данных и специальных классах.

Этот проект - только начало: есть еще много что исследовать. Почему бы не добавить поддержку объявления деструктуризации более пяти элементов массива или списка? Сделать это можно, написав операторные методы.

Оригинал статьи

Содержание