[Kotlin] NULLな値に対するif elseをどう書くべきか
はじめに
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イン・アクション
Discussion