さて、今回の目玉機能の「分解宣言」と「多重戻り値」ですが、Javaプログラマーにとってかなり違和感の大きな機能かもしれません。
多重戻り値とは端的に言うと「関数の戻り値を1つではなく、複数返せるようにする」仕組みのことです。以前からPerl、Ruby、Python、JavaScriptなどのスクリプト言語が対応していますが、JavaなどのC言語系の言語ではあまりなじみのない機能でした。
最近ではSwift、Kotlin、C++11(C++の新しいバージョン)、C# 7など多くの言語が、分解宣言と多重戻り値(ないしは類似の機能)に対応するようになっています。正確に言うとKotlinは多重戻り値には対応していませんが、これから紹介する分解宣言という機能を使うことで「関数から複数の戻り値を受け取るような書き方ができる」仕組みになっています。
分解宣言とは「オブジェクトのプロパティを複数の変数に分解して格納する仕組み」のことです。実際の例をリスト5で見てみましょう。
//2つの数の和と積を返す関数。戻り値にPair<>を使うのがポイント fun addAndMul(a: Int, b: Int): Pair<Int, Int>{ //戻り値はPairオブジェクト return Pair(a + b, a * b) } …… //複数の戻り値を返す(ように見える関数)の呼び出し //Pairオブジェクトを分解し、waとsekiに格納している val (wa, seki) = addAndMul(2, 5) println("和: $wa, 積: $seki") //「和: 7, 積: 10」
addAndMul関数は、2つの引数の和と積を返す関数です。戻り値として宣言しているPairクラスは、任意の型(Javaと同様にジェネリクスで型を指定)の値2つを格納するクラスです。ここではInt2つを格納したPairクラスを戻り値とし、return文でPairオブジェクトを作成して返しています。関数定義側では、特別な機能を使っていません。
ポイントは戻り値を受けるところで、「val (wa, seki) = addAndMul(2, 5)」のように、左辺(代入文の左側)に複数の変数が並んでいます。このように書くことで、Pairオブジェクトが分解され、格納された1つ目の値がwa、2つ目の値がsekiにそれぞれ代入されます。結果的に、関数が複数の戻り値を返すように見えますね。
リスト5を見ると「一体どうやってこんな代入ができるの?」と自然に疑問に思うことでしょう。内部的にはリスト6のような処理が行われています。
//実際には以下のような挙動(もちろんresultという変数は説明のためだけで、実際には使われない) val result = addAndMul(2, 5) //分解宣言は内部的にcomponentN関数を呼び出している val wa2 = result.component1() val seki2 = result.component2() println("和: $wa2, 積: $seki2") //「和: 7, 積: 10」
Pairクラスにはcomponent1、component2というメソッドが定義されており、それぞれ1つ目、2つ目の値を返す仕組みになっています。分解宣言は内部的にcomponent1、component2を呼び出し、値を取得しています。
さて、component1、component2という名前に心当たりがあるでしょうか。連載第4回で解説したデータクラスは「プロパティを、宣言された順番に返すcomponentNメソッド(Nには定義順の番号が入る)」が自動的に定義されることになっていました。その時は何の用途か分からなかったかもしれませんが、分解宣言と組み合わせたリスト7を見るとよく理解できることでしょう。
//データクラスは自動でcomponentNというメソッドが定義されているので分解宣言がそのまま使える data class Person(val firstName: String, val lastName: String, val age: Int) …… //データクラスのインスタンスを作る val tanaka = Person("一郎", "田中", 25) //データクラスのオブジェクトを複数の変数に分解して代入する val (mei, sei, age) = tanaka println("姓: $sei, 名: $mei, 年齢: $age") //「姓: 田中, 名: 一郎, 年齢: 25」
ここでは、Personクラスの3つのプロパティを、分解宣言を使ってmei、sei、ageにそれぞれ代入しています。分解宣言はPairクラスだけでなく、データクラスでも簡単に使えることが分かります。
なお、分解宣言はシンプルにcomponentN(N番目の値)メソッドだけを使って処理していますので、変数の名前などは考慮されていません(リスト8)。
//分解宣言は変数名などは見ておらず、順番で制御されているので注意 val (lastName, firstName, age2) = tanaka //lastName変数には1番目の値であるfirstNameプロパティ、firstName変数には2番目のlastNameプロパティの値が返る println("姓: $lastName, 名: $firstName, 年齢: $age2") //「姓: 一郎, 名: 田中, 年齢: 25」
またリスト9のように、分解宣言で受け取る変数の個数が少ない場合でもエラーにはならず、受け先のないプロパティは代入されません。一方、変数を多く指定すると、呼び出すメソッド(componentN)が存在せずエラーになります。
//受け取る変数の個数が少ない場合もエラーにはならない。受け側で足りないものは代入されない val (mei2, sei2) = tanaka println("姓: $sei2, 名: $mei2") //「姓: 田中, 名: 一郎」 //受け取る変数の個数が多いとさすがにエラーになる //val (mei3, sei3, age3, other) = tanaka
なお、Pairクラスは2つの値を保持できますが、同様に3つの値を保持できるTripleというクラスも用意されています。4つ以上の値を戻り値として返す場合はデータクラスを定義するといいでしょう。
さて、連載第2回のforのサンプル(リスト13)では、配列からインデックスと要素の両方を取得する「for((index, elem) in names.withIndex())」というコードがありましたが、ここで謎解きの時間です。
レファレンスを確認すると、配列のwithIndexメソッドはIndexedValueというクラスを、配列の個数分連続して返すようになっています。IndexedValueはデータクラスであり、Int型のindex、T型(配列の要素型)のvalueという2つのプロパティが順に定義されています。そのため、このコードは「分解宣言を使ってindex、elemという変数にそれらのプロパティを代入している」ことが分かります。
分解宣言の仕組みを解き明かしてみると意外に単純(componentNというメソッドを順に呼ぶだけ)ですが、これで多重戻り値的な使い方も可能になっています。
これまでのJavaでメソッドから複数の戻り値を返そうとすると、戻り値専用のクラスを定義したり、あるいは引数に戻り値を設定するためのオブジェクトを渡したり、と面倒な手順を取る必要がありましたが、Kotlinであればシンプルに扱えますね。ぜひ活用しましょう*2。
*2)なお、多重戻り値(的な機能)のサポートの仕方は言語ごとにアプローチが異なり、SwiftやC# 7は関数自体が直接複数の戻り値を返すように書くことができます。C++11はKotlinと同様に、戻り値は1つのままですが、受け取り側で分解する仕組みが導入されています。
Copyright © ITmedia, Inc. All Rights Reserved.