Kotlin スタートブック 一人読書会: 第 II 部 Kotlin 文法詳解 前回に引き続き Kotlin スタートブックの一人読書会を継続する。 今回は第 II 部である。第 II 部が長いので 2 回に分ける。

第 5 章 関数

可変長引数は Java だと 型... (例えば String... args) だが Kotlin だと vararg を頭につける:

fun sum(vararg ints: Int): Int

可変長引数に配列を渡す場合は先頭に * をつける。Python みたいだ:

sum(*intArrayOf(1, 2, 3))

vararg 指定された型 Int は、特化された配列クラス (IntArray) とみなされるので、IntArray のファクトリメソッド intArrayOf を使用しています。

とある。なるほど、やはり arrayOf で作成されるのは本当に Java の不自由な配列というわけだ。 確かに以下のコードを実行してみるとコンパイルエラーとなった:

sum(*arrayOf(1, 2, 3))  // Type inference failed. Expected type mismatch: inferred type is Array<Int> but IntArray was expected

結局 Java の不自由な配列 (Java のプリミティブ型とオブジェクト型でクラスが異なる) が裏に見えてしまうのはちょっとイケてない気がしてしまうが、致し方なかったのだろう。

その後再帰呼び出しの話が続くが、いきなり関数型プログラミングの話になり難しくなった。 Scala などでもそうだが巨大な再帰呼び出しでスタックオーバーフローにならないように tailrec キーワードを付けると TCO (末尾呼び出し最適化) が行われるようになる:

tailrec fun sum(numbers: List<Long>, accumulator: Long = 0): Long =
    if (numbers.isEmpty()) accumulator else sum(numbers.drop(1), accumulator + numbers.first())
    

また Kotlin でもローカル関数 (関数の中の関数) を定義できるとある。 個人的にこれも嬉しい仕様で、いわゆるヘルパー関数の影響の及ぶ範囲がわかりやすくなるのでコードが追いやすくなる。 Java 8 でも一応できる (Lambda 式をローカル変数に代入する) が書きやすいとは言い難い。

第 6 章 第一級オブジェクトとしての関数

初めに関数オブジェクトについての説明が入っていた:

Kotlin では関数も、数値や文字列などの他の値のように扱うことができます。...(中略)...このような性質を持つオブジェクトのことを、「第一級オブジェクト」(first-class object) と呼びます。

構成の関係上まだラムダ式が登場していなかったので簡単な具体例が出せなかったのだろうが、要するに以下のようなものだろう:

val plus = { x: Int, y: Int -> x + y }  // 足し算をする関数オブジェクト
print(plus(1, 2))  // 3

尚、冊子の方には関数オブジェクトという言葉は Kotlin 公式ドキュメントには登場しないので便宜上の用語として捉えて欲しい、と注釈してあった。

ただ Kotlin で関数 (関数オブジェクトではない) が定義されている場合に関数オブジェクトを取得する方法もあり、それは前置ダブルコロンで表現されるとのこと:

fun square(i: Int): Int = i * i
fun main(args: Array<String>) {
    println(::square)  // 前置 :: で関数オブジェクト取得
}

Java みたいな記法だ。 Java と同じくメソッド参照と言えば良いと思うのだが、冊子の中にはそういった記載は無い。 まぁこの場合だとメソッド参照でなく関数参照という事になるが。

その後いよいよラムダ式が登場した。

さらに、ラムダ式の引数が 1 つのときに限り、暗黙の変数 it を使用することができます。

val square2: (Int) -> Int = { it * it }

Swift だと引数が 2 個以上でも暗黙の変数 $0, $1, ... が使用できるので便利なのだが Kotlin では引数が 1 個の時しか使用できない。分かりやすさを重視したということだろうか。

その後インライン関数の説明が入っている:

インライン関数は、引数の関数オブジェクトが、コンパイル時にインライン展開される関数のことです。通常の関数に、アノテーション inline を付加するだけで、インライン関数になります。

inline はアノテーションだったのか。キーワードではなくて。

あくまで引数の関数オブジェクトがインライン展開されるようなので、引数が関数オブジェクトになっているような高階関数に使用するとパフォーマンスが向上するとのこと。以下の様な例があった:

inline fun log(debug: Boolean = true, message: () -> String) {
    // この中のコードが message() の箇所も含めて展開される
    if (debug) {
        println(message())
    }
}

コラムには同様に noinlinecrossinline もあると紹介されていた。

また、ラムダ式とは異なるものとして無名関数が紹介されていた:

val square2: (Int) -> Int = fun(i: Int): Int {
    return i * i
}

こんな書き方もできるのは初めて知ったが、ラムダ式を使わずこちらを使う意味が正直よく分からなかった。

第 7 章 オブジェクトからクラスへ

Java やその他の言語でインタフェースやクラスに馴染みのある読者は、この章を読み飛ばしてもいいでしょう。

と書いてあるようにほぼ Java と同じ概念の説明なので特筆すべきことは殆ど無い。

1 つだけ、Kotlin では Java でいうところの無名クラスの即時インスタンス化のような構文はオブジェクト式を用いて以下のように書く:

// 即時インスタンス生成
val bucket = object {
    ...
}

// Bucket インターフェース実装
val bucket = object : Bucket {
    ...
}

Android でも 2 つ以上メソッドを持つリスナの実装を行う際にこの記法が出てくる。

第 8 章 クラスとそのメンバ

プロパティの説明でバッキングフィールド (backing field) という聞き慣れない単語が出てきた。 Kotlin のクラスのプロパティ (フィールド) は自動的にバッキングフィールドが生成され、実際にはこのバッキングフィールドに値が格納されるとのこと。 バッキングフィールドを持たない例として以下の様な nameLength の例が紹介されていた:

class Person {
    var name: String = ""
        set(value) {
            println("${value}がセットされました")
            field = value
        }
    var age: Int = 0
    val nameLength: Int
        get(): Int = this.name.length
}

プロパティには上記の例のようなゲッターやセッターを定義できる。 セッター内で使用できる暗黙の変数 field に格納するとそのプロパティ値を格納したことになる。 直接 name = value などとするとセッターが無限に呼びだされ続けスタックオーバーフローになるとのこと。

プロパティは必ず初期化する必要があるが、それだと DI やユニットテストの時に不便なので lateinit キーワードを使うと初期化を遅らせることができる、という説明があった。

Kotlin におけるセカンダリコンストラクタ (2 つ以上コンストラクタを持つクラス) の定義はクラス内に constructor キーワードで定義するとのこと:

class Rational(val numerator: Int, val denominator: Int) {
    constructor(numerator: Int) :  this(numerator, 1)
}
class Rational(val numerator: Int, val denominator: Int = 1)  // この例の場合こちらの方がシンプル

また、既存のクラスを継承せずにメソッドを生やすことができる拡張関数 (エクステンション) に関して説明されていた。 Swift にもあるがとても便利だ。

第 9 章 継承と抽象クラス

Kotlin は、デフォルトではクラスを継承することができません。クラスを継承可能にするには、修飾子 open をクラスに対して付ける必要があります。

継承可能であることを明示するために open を付ける。 これは良い設計だと思う。 基本的に継承は影響が大きく、乱用するとコードがスパゲッティになりやすい。

また、オーバライド可能なメンバにも open を付けてオーバライド可能であることを明示する必要がある。

Java でいうところの全ての継承元のオブジェクトである Object にあたるのは Any である。 toString()equals()hashCode() がそれぞれ open で定義されている。

Kotlin の可視性修飾子には Java でいうところのパッケージプライベート (デフォルトのアクセス制限) とかいう訳のわからないものは勿論無く publicinternalprotectedprivate となっている。 internal は同一モジュール内に限りアクセス可能であることを示す修飾子である。 何も付けない場合はデフォルトで public となる。