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

Что такое деструктуризация?

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

data class Address(val street: String, val city: String, val state: String)

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

val (street, city, state) = user.address

Способ деструктуризации в Kotlin заключается в том, что класс должен реализовывать несколько функций «componentN». Если вы реализуете component1(), component2() и component3(), вы сможете использовать деструктурирование, чтобы получить три элемента, например val (item1, item2, item3). Kotlin предоставляет их нам, когда мы определяем любой класс данных. Первое поле - это первый компонент и так далее.

Почему это вредно?

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

data class Address(val street1: String, val street2: String, val city: String, val state: String)

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

val (street, city, state) = user.address

Это все еще компилируется и, к сожалению, ужасно неверно. Именно здесь становится очевидной разница между позиционными данными и ассоциативными данными. component2() - это не настоящая идея для Address, street2. К счастью, вы можете найти все случаи использования нового свойства и увидеть вызов деструктуризации. Вы, как лицо, вносящее изменения, по-прежнему должны вручную проверять все звонки. Зачем программисту это делать?

Как это исправить?

Есть другие языки, на которых нет этой проблемы. Один из последних языков, который мне недавно понравился, - это Elixir. В Elixir есть больше базовых типов данных для всех этих случаев. Есть списки, кортежи, списки ключевых слов, карты и структуры. Первые два позиционные, а последние три ассоциативные. Теперь, когда мы выполняем деструктуризацию, нам нужно указать метку или «атом» в Elixir. Итак, одни и те же данные в Elixir для адреса могут быть выражены в виде структуры.

defmodule Address do
  defstruct street: "", state: "", country: ""
end

Затем его можно деструктурировать с помощью

%{street: street, state: state, country: country} = user.address

Теперь, как и раньше, мы добавляем нашу новую функцию.

defmodule Address do
  defstruct street: "", street2: "", city: "", state: ""
end
%{street: street, city: city, state: state} = user.address

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

%{state: state, city: city, street: street} = user.address

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

{first, second, last} = {1,2,3} // tuples
[first, second | rest] = [1,2,3,4,5] // lists
{first, second} = user.address // ERROR
[first, second | rest] = user.address // ERROR

А как бы нам это сделать в Котлине? На самом деле это довольно хороший вопрос. В Котлине нет понятия об атомах, но у нас есть поля. Мы могли бы потребовать что-то в конце метки, чтобы очертить ее синтаксически, или мы могли бы заставить синтаксис, более похожий на карту. Что-то вроде

val (street to street, city to city, state to state) = user.address

Это как-то долго выдыхается. Вероятно, мы могли бы избежать подразумеваемого имени переменной в качестве имени поля, отбросив часть после «до».

val (street to, city to, state to) = user.address

Хотя это звучит очень странно. Это может быть невозможно даже из-за того, как реализован to. Это просто функция, которая производит пару. Такое использование to также может быть очень сложно реализовать. Мой личный выбор сегодня был бы более радикальным.

val {street to street, city to city, state to state} = user.address
val {street, city, state} = user.address

Тогда есть четкое различие между позиционным () и ассоциативным {}, но при этом допускается сокращение, когда предполагается, что имя поля является именем переменной. Может быть, стенография не имеет значения и окажется такой же плохой.

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

tl; dr. Классы не позиционные. Они ассоциативны. Рассматривая их как позиционные, вы можете создать трудно обнаруживаемые ошибки при изменении класса.