雑に描いた餅

餅は餅屋、わたしは技術屋、と言いたかった。

KotlinにおけるDestructuring declarations(分割宣言)の小ネタ

Kotlinでプログラムを書く時、しばしば使う機能としてDestructuring declarations(分割宣言)*1があります。

例えば、以下のように、data classのプロパティをそれぞれ別の変数として宣言することで、独立した変数として取り扱うことができます。

data class Person(val name: String, val age: Int)

val masuo = Person("Masuo", 29)
val (name, age) = Person("Masuo", 29)
println("$name -san is now $age years old.") // Masuo -san is now 29 years old.

最近Kotlinでコードを書く中で、改めて分割宣言と触れる機会があったので、機能的な部分のおさらいをしつつ、使いどころについて考えてみます。

Destructuring declarationsとは

「Destructuring=構造を破壊する、分割」というその名の通り、値、オブジェクト、配列を複数の変数にdestructuring=分割して宣言することができるという機能です。他の言語ではJavaScriptPHP、Rustなどなど多くの言語で採用されています。

componentN() というもの

Kotlinで宣言された分割宣言は、分割した変数ごとに component1(), component2() といった、 componentN() (ここでのNは自然数をさす)関数としてコンパイルされます。なので、変数の数が増えていけば component3(), component4()......といった具合に数字が増えていきます。

この関数はdata class以外でも、HashMapやArrayといった多くの標準ライブラリのクラスで実装されているので、特に意識せず分割代入できるクラスには大体生えている、という理解で良いと思います。

ちなみに使う場面はないと思いますが、 componentN() 関数を自分で実装してしまえば、data classでないclassやobjectであっても分割宣言を利用することができます。

object HttpClient {
    private var host by Delegates.notNull<String>()
    private var port by Delegates.notNull<Int>()
    operator fun component1(): String {
        return this.host
    }
    operator fun component2(): Int {
        return this.port
    }
}

val (host, port) = HttpClient

分割宣言のメリット

分割宣言自体、とてつもなく便利!ないと困る!ような代物ではないと思います。ただLambdaで引数として受け取る値やData classについて、それ全体ではなく、その時々の関心事だけを、見通しよく、シンプルに表現できるのが良さだと思います。

処理を書く上で、値やオブジェクトの中の特定の部分にのみ処理をかけたい、あるいはその他の部分を無視したい、というときに覚えておくと、コードで表現する力が上がると思っています。

ユースケース

以下では実際の利用例を見ていきます。

Data classのパラメータ

Data classではデフォルトで componentN() が宣言されるため、パブリックなプロパティは全て分割宣言で取り出すことができます。また、特定のプロパティのみを取り出すことも可能です。

data class Person(val name: String, val age: Int)

val (name) = Person("Katsuo", 11)
println("Hey ${name.uppercase()}!!!") // Hey KATSUO!!!

Pair型

Pair型のプロパティは first, second の名前で取り出すことができますが、分割宣言で取り出すことも可能です。以下のコードでは、下2行の出力結果は同じになります。

val product = "Apple" to 200
val (name, price) = "Apple" to 200
println("${product.first} is ${product.second} yen.")
println("$name is $price yen.")

Lambdaのパラメータ

KotlinでCollectionを操作するときに登場する map()reduce() などでお馴染みかと思いますが、Lambdaの引数も分割宣言が可能です。

val persons = listOf(Person("Masuo", 29), Person("Katsuo", 11), Person("Wakame", 9))
persons.forEach { (name, age) -> println("After 10 years, ${name.uppercase()} will be ${age + 10} years old.") }

// After 10 years, MASUO will be 39 years old.
// After 10 years, KATSUO will be 21 years old.
// After 10 years, WAKAME will be 19 years old.

なおLambda式自体は、実際に引数がいくつに分割可能なのかを意識しません。そのため、以下のように想定しないエラーが発生するケースもあります。

val numbers = listOf(listOf(1,2,3), listOf(1,2))
numbers.forEach { (a,b,c) -> println("$a + $b = $c") }

// 1 + 2 = 3
// Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2
// at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4266)

実用を考えると、そのままの値を用いるより個別のData classを作って名付けるようにするほうが良いでしょう。

アンダースコア

利用しない変数には _ をつけられます。関心を絞る意味でも、利用しないのであればアンダースコアをつけてわかりやすくしたほうが良いでしょう。

val products = mapOf("Apple" to 200, "Banana" to 300, "Orange" to 400)
products.forEach { (_, v) -> println("$v yen!!!") }

// 200 yen!!!
// 300 yen!!!
// 400 yen!!!

小ネタもいいところでしたが今日はここまで。


参考

https://kotlinlang.org/docs/destructuring-declarations.html#destructuring-in-lambdas

https://maku77.github.io/kotlin/basic/dest-decl.html

*1:Kotlinの日本語公式ドキュメントは見当たらなかったのですが、Android developersのこのページに語として登場していたので、それを用いています。