Kotlinの分解宣言(destructuring declarations)でPairとTripleの要素を分解する
PairクラスとTripleクラス
Kotlinは、2つの値または3つの値を保持するための簡単なクラスとして、PairクラスとTripleクラスを提供している。
Pairオブジェクトの1番目と2番目の要素は、それぞれfirst、secondというプロパティでアクセスできる。
val pair = "one" to 1
println(pair.first)
println(pair.second)
実行結果:
one
1
Tripleオブジェクトの要素も同じように、first、second、thirdというプロパティでアクセスできる。
val triple = Triple("one", "two", "three")
println(triple.first) //=> "one"
println(triple.second) //=> "two"
println(triple.third) //=> "three"
実行結果:
one
two
three
分解宣言でオブジェクトの各プロパティを別々の変数に代入する
Kotlinの**分解宣言(destructuring declarations)**という形式で変数を定義すると、オブジェクトの各プロパティが持つ値を一度に複数の変数へ代入できる。分解宣言の方法は簡単で、代入文の左側に複数の変数を括弧で囲んで書けばよい。
次の例では、Pairオブジェクトのfirstプロパティとsecondプロパティの値を、それぞれx、yという変数に代入している。
val pair = 100 to 200 // Pair(100, 200)と同じ
val (x, y) = pair // 分解宣言による代入
println(x)
println(y)
実行結果:
100
200
Tripleオブジェクトについても同じである。
val triple = Triple(100, 200, 300)
val (x, y, z) = triple
Pairオブジェクトを使った複数値返却関数の実装
関数がPairやTripleを返す場合でも、同じように複数の変数へ直接代入できる。これは、まるで複数の戻り値を返す関数のように動作する。
// 簡単なPair返却関数
fun getPosition(): Pair<Int, Int> = 100 to 200
// 返されたPairの各プロパティを別々の変数で受け取る。
val (x, y) = getPosition()
println(x)
println(y)
実行結果:
100
200
ただし、このように関数の戻り値を分解宣言で受け取る場合は、変数の順序を変えないよう注意が必要である。たとえば、次のように変数名を逆にすると、見つけにくいバグが発生する可能性がある。
val (y, x) = getPosition()
したがって、Pairオブジェクトを複数値返却関数の実装に使うことは、むしろアンチパターンといえる。Kotlinではデータクラスを簡単に定義できるため、一般的には次のように戻り値を表す型を定義する方が安全である。
data class Position(val x: Int, val y: Int)
fun getPosition(): Position = Position(100, 200)
val pos: Position = getPosition()
val (x, y) = getPosition()
最後の行では、先ほど説明した例と同じように分解宣言で代入しているが、変数名を入れ替えた場合にはIDE(開発環境)が警告メッセージを表示してくれるため、エラーが発生する確率はかなり低くなる。
分解宣言の仕組み
データクラスによるcomponentNの自動実装
val triple = Triple(100, 200, 300)
val (x, y, z) = triple
上のような分解宣言は、実際には内部的に次のように処理される。実際にこのように書いても動作する。
val triple = Triple(100, 200, 300)
val x = triple.component1()
val y = triple.component2()
val z = triple.component3()
このように動作するには、TripleクラスがcomponentNという関数を提供している必要があるが、Tripleクラスの定義を見てもそのような実装は見当たらない。
public data class Triple<out A, out B, out C>(
public val first: A,
public val second: B,
public val third: C
)
実はKotlinでは、クラスをデータクラスとして定義すると、そのプロパティへアクセスするためのcomponentN演算子関数が自動的に定義されるようになっている。つまり、データクラスのプロパティは基本的に分解宣言による代入が可能である。
分解宣言の代表的な使用例として、コレクション系クラスのwithIndex()関数がある。
val arr = arrayOf("one", "two", "three")
for ((index, value) in arr.withIndex()) {
println("$index -> $value")
}
実行結果:
0 -> one
1 -> two
2 -> three
withIndex()関数はIndexedValueというクラスのオブジェクトを順番に返すが、このクラスもデータクラスとして定義されているため、そのプロパティ(indexとvalue)を分解宣言で取得できる。
data class IndexedValue<out T>(public val index: Int, public val value: T)
データクラス以外でのcomponentN実装
データクラスではないオブジェクトを分解宣言の右側に置くには、componentN系の関数を自分で実装する必要がある。
次のコードはMapの要素をループ処理する例であり、ここでkeyとvalueのペアを取り出すときにも分解宣言の仕組みを活用している。
val map = mapOf(1 to "one", 2 to "two")
for ((key, value) in map) {
println("$key -> $value")
}
MapとMap.Entryはデータクラスではなくインターフェースであるため、componentN系の関数が実装されておらず、そのままでは分解宣言の右側に置くことができない。そこでKotlin標準ライブラリでは、MapインターフェースとMap.Entryインターフェースを次のように拡張し、分解宣言を使ったループ処理を可能にしている。
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
つまり、上で説明したループ処理は次のコードと同じである。
for (entry in map.entries) {
val key = entry.component1()
val value = entry.component2()
println("$key -> $value")
}