こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。
この記事は、毎週木曜の定期連載 "Weekly Report" 共有の第 45 回です。 LINEヤフー社内には、高い開発生産性を維持するための Review Committee という活動があります。ここで集まった知見を、Weekly Report と称して毎週社内に共有しており、その一部を本ブログ上でも公開しています。(Weekly Report の詳細については、過去の記事一覧を参照してください)
終わり null ならすべてよし?
Java や Kotlin の Iterator
で next
を呼ぶときは、「次」の要素が存在することを確認しなければなりません (通常は hasNext
で確認します)。以下のコードのように、「次」の要素がない状態で next
を呼び出すと、NoSuchElementException
が投げられます。
そこで、next
を安全に呼び出せるように、SafeIterator
というクラスを定義しました。このクラスは、要素の数を超えて next
を呼び出されたとき null
を返します。
以下のコードは、SafeIterator
をループで使う例です。
このようにすることで、hasNext
による事前チェックを null
のチェックで代替できます。smart cast や type guard、 safe call operator がある言語ならば、要素があることを保証しつつ、ランタイムエラーが起きえないコードを書けます。
しかし、この SafeIterator
には問題点があります。それは何でしょうか?
幕間か終演か
この SafeIterator
には、要素に nullable な型を使ったときに、要素としての null
と末尾の意味の null
を区別できないという問題があります。そのため、途中で走査を止めてしまうというバグを発生させかねません。例えば以下のコードでは、0
は出力される一方で 2
は出力されません。
一般化して言うと、コレクションやラッパーを作るときに エラー/エッジケースを意味する型と、要素の型で重複を避ける 必要があります。
Option 1: エラーとして使う型を要素から除外する
最も簡単な解決策の一つとして、要素の型を「エラー/エッジケースの型を含まないもの」だけに限定する方法が挙げられます。今回の場合は、要素が non-null であることを保証すれば十分です。Kotlin の場合は、タイプパラメータの上界として Any
を指定することで実現できます。
空の要素を表現する必要がある場合は、明示的に「空」を表現可能な型を別途用意すればよいです。以下の NullableValue
や Optional
は空を表現可能にしたクラス定義の例です。
ただし、エラー/エッジケースとして null
を使う場合、このような型を定義してもなお、言語によっては安全には取り扱えない点に注意してください。
Option 2: 戻り値専用の型を定義する
Option 1 の発想とは逆に、コレクション/ラッパー側で戻り値用の型を別途定義するという方法もあります。以下のような NextResult
を定義することで、null
の要素がある (NextResult(null)
) のか、要素自体がない (null
) のかを区別できます。
より空の要素を明示的に表現するためには、NoSuchElement
のような値を用意しても良いかもしれません。
このようにすることで、要素に null
があっても、終端と区別できるようになります。
Option 3: 関数名で注意できるようにする
Kotlin の標準的な命名として、OrNull
や OrThrow
といった表現があります。これは、要素を取得できない場合などに null
を返したり、例外を投げることを意味します。型の定義を変えるのが難しい場合は、このように名前でエラー/エッジケースの取り扱いを明確にするという方法もあります。
nextOrThrow
: 範囲外の場合はNoSuchElementException
を投げるnextOrNull
: 範囲外の場合はnull
を返す
このような名前をつけることで、nextOrThrow
は事前チェックが必要で、nextOrNull
は null
の取り扱いに注意が必要ということに気が付きやすくなります。
null は続くよどこまでも
null
の取り扱いに注意が必要なケースは、get
や next
といった戻り値だけではありません。内部的に値を保持する場合一般に注意が必要です。例えば Kotlin の Lazy
では、未初期化の状態を表現するために UNINITIALIZED_VALUE
というオブジェクトを使い、null とは明確に区別しています (cf., https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/src/kotlin/util/Lazy.kt#L93 )。
一言まとめ
コレクションやラッパーを作る場合は、エラー/エッジケースを示す型と値の型の重複に気をつける。
キーワード: null
, empty
, collection