🦑

[Kotlin] NULLな値に対するif elseをどう書くべきか

2022/11/03に公開

はじめに

Kotlinの特徴の1つにNULL安全があります。
そして、NULLを安全・便利に扱うための演算子が用意されています。

名前 演算子 実行される条件
安全呼び出し演算子[1] ?. 呼び出し元の値が非NULL
エルビス演算子 ?: 左辺の値がNULL

この演算子を利用したif elseの実装を見かけ、想定通りの挙動をしていませんでした。
周りのエンジニアもこの挙動を把握していなかったので、共有の意味でまとめてみました。

TL;DL

NULLかどうかの分岐制御にはif elseを使用すべきです。
代わりに、?.let{..} ?:を使用するのはおすすめしません。
?:の使いどころはNULL時のデフォルト値の代入など、副作用のない処理[2]に留めておきましょう。

OK🙆

if (foo != null) {
    // foo != nullの時の処理
} else {
    // foo == nullの時の処理
}

NG🙅‍

foo?.let {
    // foo != nullの時の処理
} ?: let {
    // foo == nullの時の処理
}

NULL安全な演算子で分岐処理を書く

?.を使用した非NULLな時の処理は、Kotlinのコードでよく見かけます。

foo?.let {
    // foo != nullの時の処理
    println("foo != NULL")
}

つい同じノリでif elseを書きたくなります。
エルビス演算子(?:)の出番です。

foo?.let {
    // foo != nullの時の処理
    println("foo != NULL")
} ?: let {
    // foo == nullの時の処理
    println("foo == NULL")
}

一見問題なく、if elseとして機能します。

本当にif elseの代替として問題ないのか

では、少し複雑なケースです。

  • fooがNULLかをチェック
  • そしてfoo!=NULLの時だけ、barがNULLかをチェック
foo?.let {
    println("foo != NULL")
    bar?.let {
        println("bar != NULL")
    }
} ?: let {
    println("foo == NULL")
}

foo = 100, bar = nullの時、期待通りの動きをしません。

結果は以下の通りです。
非NULL用の処理と、NULL用の処理の両方が呼ばれてしまいました。

foo != NULL
foo == NULL

何が起きたのか🤯

書きたかったコードは、fooがNULLかどうかです。
しかし、?:の判定に使用される左辺の値はfooではなくfoo?.let {..}なのです。
つまりfoo == nullに加え、let{..}の返り値がNULLの場合も実行されてしまいます。

制御構文ではなく演算子であり、上から順次実行しているだけで、冷静に考えると当たり前の挙動ですよね。
前後にスペースを空ける文法ではなく、.つなぎだったら誤解は生まれなかったかもしれないですね🤔

まとめ

NULLかどうかによる分岐制御にはif elseを使用しましょう。
letの代わりにalsoを使えば、本来やりたかったことも一応可能です。
Kotlinのifは式で値も返せますし、if elseを使う方が良さそうです。

OK🙆

if (foo != null) {
    // foo != nullの時の処理
} else {
    // foo == nullの時の処理
}

NG🙅‍

foo?.let {
    // foo != nullの時の処理
} ?: let {
    // foo == nullの時の処理
}

補足

また、意図せぬ挙動をから良くないという話ではないと考えています。
ちゃんと目的に沿った文法を使うべきであるということを念押ししたいです。
サンプルの処理も本来はif使うべきだと心の中では思っています。

if (foo != null) {
    // foo != nullの時の処理
    println("foo != NULL")
}

if elseは制御構文であり、?.?:は演算子です。
三項演算子でfoo ? funA() : funB()なんて実装はしないですよね…?[3]

参考

Kotlinでif let elseをやりたいときはletでなくalsoを使おう
Kotlinイン・アクション

脚注
  1. safe call operatorの一般的な日本語訳があれば教えてください…。 ↩︎

  2. 関数の呼び出しや他の値の書き換え等をしない処理。 ↩︎

  3. funA()かfunB()の返り値を変数に詰めるケースならありか…?🙄 ↩︎

Discussion