Decomposing Pair and Triple Elements with Kotlin Destructuring Declarations
Pair and Triple Classes
Kotlin provides the Pair and Triple classes as simple classes for holding two or three values.
The first and second elements of a Pair object can be accessed through the first and second properties.
val pair = "one" to 1
println(pair.first)
println(pair.second)
Execution result:
one
1
The elements of a Triple object can likewise be accessed through the first, second, and third properties.
val triple = Triple("one", "two", "three")
println(triple.first) //=> "one"
println(triple.second) //=> "two"
println(triple.third) //=> "three"
Execution result:
one
two
three
Assigning Each Property of an Object to Separate Variables with Destructuring Declarations
When variables are defined in Kotlin using destructuring declarations, the values held by each property of an object can be assigned to multiple variables at once. The syntax is simple: write multiple variables wrapped in parentheses on the left side of an assignment.
In the following example, the values of the first and second properties of a Pair object are assigned to variables named x and y.
val pair = 100 to 200 // Same as Pair(100, 200)
val (x, y) = pair // Destructuring declaration assignment
println(x)
println(y)
Execution result:
100
200
The same applies to Triple objects.
val triple = Triple(100, 200, 300)
val (x, y, z) = triple
Implementing Multi-Value Functions with Pair Objects
Even when a function returns a Pair or Triple, its values can be assigned directly to multiple variables in the same way. This behaves like a function that returns multiple values.
// Simple function that returns a Pair
fun getPosition(): Pair<Int, Int> = 100 to 200
// Receive each property of the returned Pair as a separate variable.
val (x, y) = getPosition()
println(x)
println(y)
Execution result:
100
200
However, when receiving a function’s return value through destructuring declarations, be careful not to change the variable order. For example, reversing the variable names as follows can create bugs that are difficult to find.
val (y, x) = getPosition()
Therefore, using Pair objects to implement functions that return multiple values can actually be considered an anti-pattern. Since Kotlin makes it easy to define data classes, it is generally safer to define a type that represents the return value as follows.
data class Position(val x: Int, val y: Int)
fun getPosition(): Position = Position(100, 200)
val pos: Position = getPosition()
val (x, y) = getPosition()
In the last line, assignment is performed through destructuring declarations as in the earlier example, but if you reverse the variable names, the IDE displays a warning message, making errors much less likely.
Structure of Destructuring Declarations
Automatic Implementation of componentN Through Data Classes
val triple = Triple(100, 200, 300)
val (x, y, z) = triple
The destructuring declaration above is actually handled internally as follows. It also works if written this way.
val triple = Triple(100, 200, 300)
val x = triple.component1()
val y = triple.component2()
val z = triple.component3()
For this to work, the Triple class must provide functions named componentN, but if you look at the definition of the Triple class, you cannot find such an implementation.
public data class Triple<out A, out B, out C>(
public val first: A,
public val second: B,
public val third: C
)
In fact, when a class is defined as a data class in Kotlin, operator functions named componentN for accessing its properties are automatically defined. In other words, properties of data classes can basically be assigned through destructuring declarations.
A representative use of destructuring declarations is the withIndex() function of collection classes.
val arr = arrayOf("one", "two", "three")
for ((index, value) in arr.withIndex()) {
println("$index -> $value")
}
Execution result:
0 -> one
1 -> two
2 -> three
The withIndex() function returns objects of the IndexedValue class in order. Since this class is also defined as a data class, its properties (index and value) can be obtained through destructuring declarations.
data class IndexedValue<out T>(public val index: Int, public val value: T)
Implementing componentN Outside Data Classes
To place a non-data-class object on the right side of a destructuring declaration, you must implement the componentN family of functions yourself.
The following code is an example of looping through elements of a Map. The mechanism of destructuring declarations is also used when extracting the key and value pair.
val map = mapOf(1 to "one", 2 to "two")
for ((key, value) in map) {
println("$key -> $value")
}
Because Map and Map.Entry are interfaces rather than data classes, the componentN family of functions is not implemented, so they cannot be placed on the right side of a destructuring declaration as-is. Therefore, the Kotlin standard library extends the Map and Map.Entry interfaces as follows so loops using destructuring declarations are possible.
inline operator fun <K, V> Map<out K, V>.iterator(): Iterator<Map.Entry<K, V>> = entries.iterator()
inline operator fun <K, V> Map.Entry<K, V>.component1(): K = key
inline operator fun <K, V> Map.Entry<K, V>.component2(): V = value
In other words, the loop described above is equivalent to the following code.
for (entry in map.entries) {
val key = entry.component1()
val value = entry.component2()
println("$key -> $value")
}