オプショナル型の値が入っていたら何か処理をする

Kotlin には let という構文がある。Swift だと if-let 文が相当するし Java だと Optional#ifPresent() だ。 あるオプショナル型の変数に値が入っていたら何かする、といった具合に使用する:

val a: String? = null
a?.let { print(it) }  // let ブロック内で it で optional が外された形で使用可能
a?.let { print(it) } ?: print("nothing")  // elvis 演算子と組み合わせる事で null の場合の処理も記述可能

この a?.let が曲者な気がする。

typo するとバグの元になりそう

上記の例で以下のように間違えると意図した結果にならない:

val a: String? = null
a.let { print(it) }  // ? が無いので a が null でも print されてしまう!!
a?.let { print(it) }  // ? があるので正しく print されない

これでうっかりハマって暫く気づかなかった……。 ちなみに let のブロック内で it のメソッド呼び出しをしようとするとオプショナル型がアンラップされていないのに気づいてコンパイルエラーになる:

val a: String? = null
a.let { it.toInt() }  // オプショナル型 it への安全でないメソッド呼び出しでコンパイルエラー
a?.let { it.toInt() }  // これは OK (it.toInt() は実行されない)

Swift や Java ではこういうことはない

Swift の if-let ではこういうことはないし Java だとそもそも Optional 型でないと ifPresent メソッドをコールできないのでこういうことはない。

カラクリ

ソースを見ると実は Kotlin の let言語構造的なものではなく拡張メソッド (エクステンション) を使用して以下のように定義されている:

@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R = block(this)  // 渡された関数オブジェクトを実行するだけ!!

この定義を見ると何てことはなく、ただ単に引数に渡された関数オブジェクトを実行するだけの拡張メソッドである。 つまり String? 型として let を呼べるし、呼ぶと上記 block の型は (String?) -> R となるだけのことだった。

let は実は以下の省略形と書けば理解が進む気がする:

val a: String? = null
a?.let { print(it) } // せいかい. 実は (String) -> Unit
a?.let { b: String -> print(b) }  // 冗長な書き方. 上と同じ
a.let { print(it) }  // 間違い. 実は (String?) -> Unit
a.let { b: String? -> print(b) }  // 冗長な書き方. 上と同じ