Android Studioを使い、Kotlin言語の特徴を解説する本連載「Android Studioで始めるKotlin入門」。連載最終回となる今回は、前回までで紹介し切れなかったKotlinの特徴的な機能を解説します。個人的なイチオシは「分解宣言と多重戻り値」です。Javaプログラマーにとってはちょっと驚く機能かもしれません。
拡張関数
 Kotlinの拡張関数は、あるクラスを継承することなく、そのクラスにメソッドを追加する(ように見せる)機能です。例えばリスト1では、Catクラスにrunというメソッドを追加しています。
class Cat(val name: String){
 private val age = 10
}
//拡張関数。Catクラスの外にある関数で、Catクラスを拡張している
fun Cat.run(){
 //拡張関数の中では、thisを経由してCatクラスのプロパティを参照できる
 println("${this.name} is Running!")
 //エラー。privateプロパティは参照できない
 //println("${this.name}'s age is ${this.age}")
}
……
val cat = Cat("Tama")
//拡張関数の呼び出し例。Catクラスにrunメソッドが存在するように書ける
cat.run() //「Tama is Running!」
リスト1 拡張関数の定義例
 ここでは、Catクラスにrunメソッドを追加し、Catクラスのnameプロパティを参照しています。拡張関数で対象のオブジェクト(「レシーバーオブジェクト」と呼ばれる)を参照する場合はthisキーワードを使います。
継承不可クラスに関数を追加する
 リスト1でCatクラスにopenキーワードが付いていない、つまり継承不可であるにもかかわらず、runメソッドを追加できていることに驚かれるかもしれません。拡張関数は実際に対象のクラスにメソッドを追加しているわけではなく、外部に定義した関数を、対象となるクラスのメソッドのように見せ掛けているだけです。
 その証拠に、Catクラスにprivateとして定義したageプロパティについては、拡張関数から参照することはできず、エラーとなります。拡張関数が本当にクラスのメソッドに追加されているわけではなく、あくまでも外部で定義した関数であることが分かるでしょう。
 拡張関数の対象は自作のクラスだけに制限されておらず、Stringクラスのような標準ライブラリにも問題なく適用できます。例えばリスト2ではStringクラスにdumpというメソッドを追加しています。
//エラー。Stringクラスはfinalなので継承できない
//class MyString: String(){}

//拡張関数。Stringクラスにdumpというメソッドを追加する(ように見せる)
fun String.dump(){
 println("Length: ${this.length}, Content: $this")
}
……
//Stringクラスに拡張関数として追加したdumpメソッドを呼ぶ
"Hello".dump() //「Length: 5, Content: Hello」
リスト2 Stringクラスを拡張する
 Stringクラス自体はfinalつまり継承不可能なクラスであるため、本来は直接継承してメソッドを追加できませんが、拡張関数を使うことで、自由にメソッドを拡張できます。このように拡張関数は、人が作ったライブラリについても、自分好みの関数を追加していける魅力的な機能です。Kotlin付属ライブラリでも多数の拡張関数が使われています。

 とはいえ、あまり拡張関数を乱用すると他の人から読みづらいコードになる場合があるので、ご注意ください。
C#の「拡張メソッド」との違い
 なお、C#にも「拡張メソッド」という同じような機能があります。ただしC#の場合はいったん何らかのクラスを入れ物として用意し、そのクラスの中にstaticメソッドとして拡張メソッドを定義する必要があります。クラス外に関数を定義できないC#としては仕方ないところですが、わざわざ入れ物の(それ自体を使うことはあまりない)クラスが必要なのが面倒なところです。
 一方、Kotlinはもともとクラス外に関数を書けるため、拡張関数をより直感的に定義できます。
範囲(Range)
 連載第2回で「for」を説明する際に触れましたが、Kotlinには「範囲」(Range)という概念があり、数値などの範囲を表現できます。幾つかの使い方を見てみましょう(リスト3)。
//1〜10の範囲
for(i in 1..10){
 print(i) //「12345678910」
}

//2〜10の範囲で2刻み
for(i in 2..10 step 2){
 print(i) //「246810」
}

//9〜1の範囲(逆順)で2刻み
for(i in 9 downTo 1 step 2){
 print(i) //「97531」
}
println()

//1〜10の範囲。ただし終わりの値(10)は含まない
for(i in 1 until 10){
 print(i) //「123456789」
}
println()
リスト3 数値範囲の幾つかのパターン
 最初は基本パターンで、1〜10までを順に出力しています。「..」演算子を使って「1から10まで」を表す範囲を表現し、in演算子でそこから1つずつ値を取り出して処理しています。2つ目はstepを使って「2〜10の範囲。ただし2刻みで」という範囲を表現しています。3つ目は逆順を表すdownToを使い「9〜1の範囲(逆順)で2刻み」という範囲を表現しています。4つ目はuntilを使い「1〜10の範囲。ただし終わりの値(10)は含まない」という範囲を表現しています*1。
*1)なお、..やinは演算子ですが、step、downTo、untilなどは言語埋め込みのキーワードではなく拡張関数です。最後に触れる中置記法を使用しています。
文字範囲
 「範囲」は整数値で使うことが多いですが、文字を表すCharクラスも(内部的に整数値であるため)対象とできます。リスト4は文字範囲を使ったサンプルです。
//a〜zの範囲
for(i in 'a'..'z'){
 print(i) // 「abcdefghijklmnopqrstuvwxyz」
}

//A〜zの範囲で2刻み
for(i in 'A' until 'z' step 2){
 print(i) // 「ACEGIKMOQSUWY[]_acegikmoqsuwy」文字コード的に大文字と小文字の間の文字も出力されている
}
リスト4 文字範囲のサンプル
 1つ目はaからzまで、2つ目はAからzまでで2刻みの範囲を表現しています。
 「範囲」はforと組み合わせて繰り返しを表現する他に、条件判定にも使うことが多いでしょう。連載第2回で紹介した「when」のサンプルであるリスト9では、in演算子を使って、値が範囲内、範囲外にあるかどうかを判定していました。
 こうした条件判定を範囲なしで書こうとすると、等号不等号とand、orなどの論理演算子を駆使する必要があるでしょう。等号不等号の組み合わせでバグを作り込むこともあるため、より直感的な書き方ができる「範囲」は便利ですね。
範囲の用途と注意点
 まとめると、範囲には以下の2つの用途があります。
反復処理する
範囲チェックする
 このうち範囲チェックについては、整数値以外にもStringやDoubleなど、比較可能な任意のデータ型に適用可能です。しかし、forで使用するような反復処理については、Int、Long、Charクラスなどが返す整数型範囲(IntRange、LongRange、CharRangeというクラス)専用の機能になっていますのでご注意ください。

分解宣言と多重戻り値(的な使い方)
 さて、今回の目玉機能の「分解宣言」と「多重戻り値」ですが、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」
リスト5 複数の戻り値を返す(ように見える関数)
 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」
リスト6 分解宣言の挙動
 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」
リスト7 データクラスを分解宣言で複数の変数に代入する
 ここでは、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」
リスト8 変数名は考慮されない
 またリスト9のように、分解宣言で受け取る変数の個数が少ない場合でもエラーにはならず、受け先のないプロパティは代入されません。一方、変数を多く指定すると、呼び出すメソッド(componentN)が存在せずエラーになります。
//受け取る変数の個数が少ない場合もエラーにはならない。受け側で足りないものは代入されない
val (mei2, sei2) = tanaka
println("姓: $sei2, 名: $mei2") //「姓: 田中, 名: 一郎」

//受け取る変数の個数が多いとさすがにエラーになる
//val (mei3, sei3, age3, other) = tanaka
リスト9 分解宣言で変数が少ない場合、多い場合
 なお、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つのままですが、受け取り側で分解する仕組みが導入されています。

演算子オーバーロード
 C++やC#などのオブジェクト指向言語で一般的ながら、Javaでサポートされていない機能の1つが「演算子オーバーロード」です。端的に言えば「自分で定義したデータ型についても、組み込みデータ型と同様に演算子を使えるようにする」機能です。
 例として複素数を表現するクラスを自分で定義するケースを考えてみましょう。リスト10は演算子オーバーロードが使える言語、使えない言語でのサンプルです。
//C#的なサンプル。演算子オーバーロードが使える場合
var c1 = new Complex(1, 2);
var c2 = new Complex(-1, 2);
var c3 = c1 + c2; //普通に+演算子が使える

//Java的なサンプル。演算子オーバーロードが使えない場合
Complex c1 = new Complex(1, 2);
Complex c2 = new Complex(-1, 2);
Complex c3 = c1.add(c2); //演算子の代わりにaddメソッドで加算する
リスト10 演算子オーバーロードが使える言語、使えない言語での書き方の違い
 演算子オーバーロードがサポートされている言語であれば、+、-などの演算子をどのように処理するかも自分で実装できるので、数値型などと同じような使い方ができます。
 一方、演算子オーバーロードをサポートしない言語の場合、add、subなどの演算子に相当するメソッドを定義し、演算子を使う代わりにそれらのメソッドを使って書かなければなりません。
 演算子オーバーロードが使えると、より直感的に記述できることがよく分かりますね。
 Kotlinではoperatorキーワードを使って、メソッドのように演算子オーバーロードを定義します。リスト11では、自作の複素数クラス(整数のみ対応)で+演算子、-演算子(2項)、-演算子(単項)を定義しています。
//自作の複素数クラス(整数のみ対応)
data class Complex(val real: Int, val image: Int){
 //operatorキーワードを使って+演算子をオーバーロード
 operator fun plus(value: Complex): Complex{
   return Complex(real + value.real, image + value.image)
 }
 //-演算子(2項)
 operator fun minus(value: Complex): Complex{
   return Complex(real - value.real, image - value.image)
 }
 //-演算子(単項)
 operator fun unaryMinus(): Complex{
   return Complex(-real, -image)
 }
 //複素数的な表記を返すように文字列化
 override fun toString(): String{
   if(image > 0)
     return "$real+${image}i"
   else
     return "$real${image}i"
 }
}
……
//複素数クラスを使ってみる
var c1 = Complex( 2, 2)  //  2+2i
var c2 = Complex(-1, 5)  // -1+5i
var c3 = c1 + c2 //普通に+演算子が使える

println(c3)      //加算結果     「1+7i」
println(c1 - c2) //減算結果     「3-3i」
println(-c1)     //単項の-演算子「-2-2i」
リスト11 複素数クラスでの演算子オーバーロードの例
 ここでは、+演算子、-演算子(2項)、-演算子(単項)に対応するメソッドとして、operatorキーワードを使ってplus、minus、unaryMinusメソッドを記述しています。
演算子をオーバーロードする際のメソッド名の一覧
 メソッド名は演算子ごとに表1のように定められています。
表1 演算子をオーバーロードする際のメソッド名
演算子
メソッド名
+
plus


minus
*
times
/
div
%
rem
+=
plusAssign
-=
minusAssign
*=
timesAssign
/=
divAssign
%=
remAssign
++
inc
--
dec
+(単項)
unaryPlus
-(単項)
unaryMinus
..
rangeTo
!
not
in
contains
[]
get / set
()
invoke
==
equals
>、<
compareTo

 四則演算子などはごく自然な対応だと思いますが、配列アクセスのように使える[]演算子、果ては関数呼び出しに相当する()までオーバーロードが可能というのは驚きですね*3。

*3)概ねC#よりも演算子オーバーロード可能な演算子は多めで、C++に近いようです。
 実は前述の「範囲」で使用する..演算子やin演算子もオーバーロードで実現されています。本連載でここまで解説してきたKotlinの言語機能の幾つかについても、演算子オーバーロードが活用されています。
注意点
 このように、演算子オーバーロードは強力で便利な機能ですが、演算子の実装の仕方によっては、データ型の利用者が想定しない挙動になることもあります。直感に反する実装や、予期しない副作用を起こすような実装は避けましょう。
その他注目のKotlinの言語機能
 全6回にわたってKotlin言語について解説してきましたが、いかがだったでしょうか。扱い切れなかった機能もまだまだあります。注目株をご紹介しましょう。
可変長引数と名前付き引数
 Java 5から可変長引数がサポートされていますが、Kotlinでも関数で可変長引数を使うことができます。また、名前付き引数は引数のデフォルト値と組み合わせるケースで引数の対応関係がとても分かりやすくなります。
中置記法(infix notations)
 一定条件を満たすメソッド(拡張関数含む)を、「obj.methodA(param)」と書く代わりに「obj methodA param」のように書くことができます。
 例えば「範囲」のサンプルとして示した「9 downTo 1 step 2」のdownToやstepはKotlin言語自体に備わったキーワードや演算子ではなく、拡張関数の呼び出しを中置記法で書いたものです。中置記法を使わずに書くと「9.downTo(1).step(2)」のようになります。言語をむやみに拡張せずに自由度の高い書き方ができる、見事な機能ですね。
委譲プロパティ(Delegated Properties)
 プロパティのアクセサ処理を他のオブジェクトに委譲する機能で、これによってアクセス時に初めて計算される遅延プロパティなどが実現できます。
 その他、インライン関数(関数の中身が呼び出し場所に展開される)やスコープ関数(オブジェクトに複数の処理をまとめて適用するなど多彩な機能)、型安全ビルダー(HTMLなどのマークアップ言語をサクサク書ける)など、Javaにはない興味深い機能もあるので、関心があればぜひレファレンスなどを参照してください。
終わりに:Kotlinに限らず、新しいプログラミング言語との向き合い方について
 さて本連載では、これまで見てきた通り、KotlinはJavaより「簡潔」「直感的」「書きやすい」「読みやすい」という言葉を何度も繰り返してきましたが、これは何もJavaをむやみにおとしめてKotlinを褒めちぎろうとしているわけではありません。
 Javaも初期バージョンからすれば多彩な言語機能をのみ込み続け、より高機能な言語へと進化し続けています*4。とはいえ、言語設計時にあらかじめ想定されていなかった機能を、互換性を保ちながら後付けすることは、ある場合には非常に困難で、非直感的な実装にならざるを得ない場合もあります。
*4)ローカル変数の型推論がJava SE 10でサポートされたのはその良い例ですね。
 一方で、KotlinやSwiftなど、新たに生まれた言語が、新しい機能を想定して(あるいはさらなる拡張を見据えて)言語仕様を決めることで、より簡潔で可読性の高い記述にできるのは当然のことといえるでしょう。
 本連載でもJavaを中心にC#など他言語との比較を取り上げましたし、「Kotlinを使えばやゆされなくなる」などというフレーズも含めてみましたが、言語間の差異を何か宗教論争のように取り上げることは本意ではありません。前述の通り、特定の分野での言語の優劣は登場時期に起因する場合もあります。そうしたことを踏まえて「言語が優れている=その言語を使うプログラマーが優れている」などという勘違いに陥らないようにしたいものです。
 ともあれ、新しい言語の多彩な機能に触れるのはプログラマーにとって刺激的で、新鮮な体験だと思います。本連載がKotlin開発に取り組む皆さまの一助となることを祈りつつ、連載の結びといたします。