Destructuring declarations

Destructuring Declarations

Posted on by Andras Kindler

Destructuring declarations, or destructuring for short, is a technique for unpacking a class instance into separate variables. This means that you can take an object, and create standalone variables from its class variables, with just a single line of code. Sounds awesome, and indeed it comes with quite a few awesome use cases, but using it can be dangerous as well.

Basics

The idea is that you can declare new stand-alone variables in a tuple-like manner, where each variable is equal to the class variable at the same position.

For example, in the next block, the author equals the variable at the first position of a Book, and so on.

data class Book(val author: String, val title: String, val year: Int)

// ...
val book = Book("Roberto Bolano", "2666", 2004)

val (author, title, year) = book
if (year > 2016) {
    // ...
}

Destructuring can be also used for:

  • Simplifying loops on Collections and Maps:
val books = listOf<Book>()
for ((author, title, year) in books) {
    // ...
}
  • Destructuring can also make lambdas more pleasant:
books.map { (author, title, year) ->  
    // ...
}
  • Returning multiple values from a function:
data class ApiResult(val code: Int, val message: String, val response: ResponseObject)

fun callApi(): ApiResult {
    return ApiResult(code, message, response)
}

// ...
val (code, message, response) = callApi()

Skipping unnecessary variables

An unused component can be replaced by an underscore ( _ ), for better readability and a more concise codebase. Very cool!

val (_, _, year) = book
    
for ((_, title, _) in books) {
    // ...
}

books.map { (_, _, year) ->  
    // ...
}

Under the hood

To understand how destructuring declaration works, let's take a look at the decompiled code of the Book class! One can find the following three functions:

@NotNull
public final String getAuthor() {
  return this.author;
}

@NotNull
public final String getTitle() {
  return this.title;
}

public final int getYear() {
  return this.year;
}

These are the so-called componentN() functions, generated automatically by data classes. When using destructuring, the corresponding componentN() is called for each variable - component1() for the first one, and so on. So our first example (val (author, title, year) = book) translates to the following:

String var2 = book.component1();
String var3 = book.component2();
int year = book.component3();
if (year > 2016) {
  // ...
}

And this is what happens when skipping the first two components:

int year = book.component3();
if (year > 2016) {
  // ...
}

Destructuring without data classes

Destructuring can be used without data classes as well. In this case, componentN() functions have to be implemented by hand, complete with the operator keyword.

For example, let's take a look at our existing Book class, without the data prefix!

class Book(val author: String, val title: String, val year: Int) {
    operator fun component1(): String = author
    operator fun component2(): String = title
    operator fun component3(): Int = year
}

⚠️ Danger zone

The way componentN() functions work is by relying on the position of each class variable. This is a problem, because a class is not meant to be positional, since the name and the value for each variable represent meaning together, not the position. This can result in bugs that are hard to identify.

Messing up the sequence of destructured identifiers will result in a semantical issue. Take a look at the following example:

val (title, author, year) = book

Even though component1() and componend2() return author and title, respectively, but the code above switches these. And it compiles, resulting in a whole lot of confusion.

Hint: if we the name of the identifier and the class parameter is the same (eg. both is called author), IDEA will present a warning message (Variable name 'author' matches the name of a different component). However, if they're not the same, no indication of a possible mixup is presented.

Problems arise when the developer modifies class signature in any way as well. Think about adding a new variable, reordering the current ones, etc. We've added a new parameter to the Book class:

data class Book(val publisher: String, val author: String, val title: String, val year: Int)

Every bit of the previously written code compiles, but is flawed: ever since the update, component1() returns publisher instead of author, and so on.

Should I use it?

Yes, but thread lightly! Destructuring is a powerful language feature, and using it can result in more concise and easy-to-read code. However, its positional mechanism is inherently different than the associational meaning of class variables, and can result in bugs that are hard to identify.