Destructuring: Unpacking an Object in One Line
Pulling several values out of an object with destructuring declarations, the componentN convention behind it, use in loops and lambdas, and the positional footgun that makes it wrong for wide objects.
Returning more than one value from a method in Java is awkward: you build a little class, abuse an array, or reach for Map.Entry, and on the receiving end you pull the pieces out one accessor at a time. Kotlin unpacks an object into several variables in a single line. It’s a small feature that shows up constantly once you have it, with one sharp edge worth respecting. Everything here was run against Kotlin 2.4.10.
This chapter follows equality and leans on the data class.
The basic form
A destructuring declaration assigns several variables from one object, names in parentheses:
val (id, name) = user
That’s shorthand for val id = user.component1() and val name = user.component2(). The unpacking is positional — id gets the first component, name the second — and the variable names are yours; they don’t have to match the property names. That freedom is also the trap, as we’ll see.
Where the components come from
Destructuring works on any type providing component1(), component2(), and so on. You rarely write those, because a data class generates one per primary-constructor property:
data class User(val id: Int, val name: String)
val (id, name) = User(1, "Ada")
This is the clean way to return several values — declare a small data class, return it, let the caller destructure:
data class MinMax(val min: Int, val max: Int)
fun range(numbers: List<Int>) = MinMax(numbers.min(), numbers.max())
val (low, high) = range(scores)
Pair and Triple work too, but a named data class reads far better than Pair<Int, Int> at the call site, where first and second tell you nothing about which is which.
In loops
Destructuring earns its place in for loops. A map yields entries, and Map.Entry provides component1/component2, so each entry splits into key and value:
for ((name, age) in ages) println("$name is $age")
withIndex() does the same with an index and value:
for ((i, name) in names.withIndex()) println("$i: $name")
Skipping components
If you don’t need one, name it _ and the compiler skips that componentN call:
val (_, name, email) = user // ignore the first component
The positional footgun
Here’s the edge that matters, and it’s a direct consequence of “the names are yours.” Destructuring binds by position, not by property name — so if you reorder a data class’s constructor, every positional destructuring keeps compiling and silently binds the wrong values:
data class Point(val x: Int, val y: Int)
val (x, y) = Point(1, 2) // x=1, y=2
// someone later swaps the constructor to Point(val y: Int, val x: Int)
val (x, y) = Point(1, 2) // still compiles — but now x=2, y=1
Nothing warns you. The variable named x now holds what used to be y. This is why positional destructuring is safe for small objects with an obvious, stable order — a Pair, a two-field result, a map entry — and dangerous for wide domain objects, where a reorder is a plausible refactor and the miswiring is invisible. For anything beyond a few fields, access by name (user.email) instead. The rule of thumb: destructure when the shape is a tuple in spirit; use named access when it’s a record.
Your own components
Destructuring works on anything defining componentN operator functions. A data class writes them, but you can add them to a regular class — or to a type you don’t own, via an extension:
class Color(val rgb: Int) {
operator fun component1() = (rgb shr 16) and 0xFF // red
operator fun component2() = (rgb shr 8) and 0xFF // green
operator fun component3() = rgb and 0xFF // blue
}
val (r, g, b) = Color(0xFF8800)
This is the same operator mechanism behind + and [] from the operators chapter — destructuring is sugar over a naming convention, no magic.
In lambdas
A lambda parameter can be destructured, so transformations over pairs or data objects read cleanly:
users.forEach { (id, name) -> println("$id: $name") }
users.associate { (id, name) -> name to id } // {"Ada"=1, "Linus"=2}
A worked example
Destructuring shines where data arrives in clumps — split a line, name the pieces:
fun parse(line: String): Pair<String, String> {
val (key, value) = line.split("=", limit = 2) // a List destructures too
return key.trim() to value.trim()
}
split returns a List, and List itself provides component1 through component5, which is why this works (and why destructuring more than five list elements won’t compile).
Final thoughts
Destructuring is syntactic sugar over the componentN convention, at its best exactly where it’s safe: small data classes, pairs, and map entries, where the order is obvious and there are only a few components. Used there, it turns multi-value returns and map iteration into clean one-liners. Pushed onto wide objects, its positional binding trades that clarity for a silent-miswire hazard on the next reorder — so keep it small, and reach for named access when the object is really a record.
Next, something more structural: how Kotlin controls what’s visible to whom, including a visibility level Java doesn’t have. That’s visibility and packages.
Practice: reinforce this with the companion workbook — short, click-to-reveal problems.
Comments