ticktakclockの日記

技術ポエムを綴ったりします。GitHub idも同じです (@ticktakclock)

Kotlinのif elseとletとalso

こんにちは、tkyです。

Kotlinにおいてnullチェックしてそのインスタンスにアクセスする場合、?.let{} を使うことがわりと多いです。

しかしnullだったときの処理も記述する場合?.let{} だとどうしてもわかりづらくなる場合があり、素直にif elseを使ったほうが読みやすいのではないかな〜という思いから記事書いてみました。

いろいろなNullチェック手法

いくつか書いてみました。わかりやすくするために関数で書いています。

このケースならパターン2の早期リターンが一番効率的だと思いますが、 Androidでいうところの context? などフレームワークで定義されている変数はnullableなことがままあり、 そういったものであればパターン3( context?.let{} )で書くことが多いです。

private fun printMessageLength1(message: String?) {
    // パターン1 if文
    if (message != null) {
        println(message.length) // smart castにより安全にmessageにアクセス
    }
}

private fun printMessageLength2(message: String?) {
    // パターン2 エルビス演算子による早期リターン
    message ?: return
    println(message.length) // smart castにより安全にmessageにアクセス
}

private fun printMessageLength3(message: String?) {
    // パターン3 ?.letを使う
    message?.let {
         println(it.length)
    }
}

private fun printMessageLength4(message: String?) {
    // パターン4 ?.alsoを使う
    message?.also {
        println(it.length)
    }
}

nullだったときに別の処理をする場合

nullだったときに"message is null" と表示してみましょう。

private fun printMessageLength1(message: String?) {
    // パターン1 if文
    if (message != null) {
        println(message.length) // smart castにより安全にmessageにアクセス
    } else {
        println("message is null")
    }
}

private fun printMessageLength2(message: String?) {
    // パターン2 エルビス演算子による早期リターン 複数行あるのでrun{}で書く
    message ?: run {
        println("message is null")
        return
    }
    println(message.length) // smart castにより安全にmessageにアクセス
}

private fun printMessageLength3(message: String?) {
    // パターン3 ?.letを使う
    message?.let {
        println(it.length)
    } ?: println("message is null")
}

private fun printMessageLength4(message: String?) {
    // パターン4 ?.alsoを使う
    message?.also {
        println(it.length)
    } ?: println("message is null")
}

なお、エルビス演算子で複数行扱いたいとき run{} を使うことが多いかなと思います

    // 複数行処理したい場合はrun{}
    message?.let {
        println(it.length)
    } ?: run {
        println("message is null")
    }

let と also の違い

一見、パターン3とパターン4に違いが殆どないように感じます。が、違いは戻り値にあります。

  • public inline fun <T, R> T.let(block: (T) -> R): R
  • public inline fun <T> T.also(block: (T) -> Unit): T

引数として渡る値はどちらもTオブジェクト(今回の場合String?です,nullチェックされているのでStringです)

戻り値はletの場合任意に指定でき、alsoの場合Tオブジェクト(今回の場合String?です)

この前提を踏まえるとletの場合ちょっと問題が起こります。

printMessageLength3("hoge")

private fun printMessageLength3(message: String?) {
    message?.let {
        println(it.length)
        null
    } ?: run {
        println("message is null")
    }
}

出力

I/System.out: 4
I/System.out: message is null

messageがnon-nullなのに、let{}句内の記述内容によって動作が変化してしまうことです。

実際こんなコードを書くことはほぼないのですが、 fun doSomething(): String? のような関数がもし存在してlet内に記述されてしまったら 意図せぬところで不具合が起こることになります。

letとエルビス演算子の組み合わせ(?.let{} ?: run{} )はパット見問題なさそうに見えるが故にすぐに不具合に気づけない可能性も秘めているので 使うならalsoとエルビス演算子の組み合わせ(?.also{} ?: run{})が良いかなと思います。

まとめ

also 「また」は中学で習う英単語です。「AのほかにBもまた○○である」という表現で使われます。

message?.also{ // A}: run{ // B }

「メッセージがnullでなけばAもまた行う。」

ある物と別の物に対して言及するときに使うイメージがあるので若干違和感がありますね。。僕だけかな。。。

なので純粋に if(message != null) { //A } else { //B } と書いたほうが他の実装者に意図も伝えやすく不具合が起こりにくくなるのではないかな、ということを考えながらプログラミングしています。