Rustの公式ドキュメントにnullに関する興味深い記事("Null References: The Billion Dollar Mistake"(Null参照: 10億ドルの間違い))が引用されています。
Enumを定義する - The Rust Programming Language
nullは、インタフェースにおける双方の合意として取り得る値とするか、しないかがいつもあいまいになってしまう点が問題。ある変数の型を見てもnullを取り得るかは明示されていないにも関わらず、人によって「nullを考慮すべき」「nullは取り得ない」の二つの状態が混在してしまう。たとえ、同じ人が双方のコードを書いていたとしても。
ScalaにおけるNull型のクラス階層における位置付けと、nullの代入
https://docs.scala-lang.org/tour/unified-types.html
Scalaにおける型の階層ではNull型はAnyRef
のサブタイプになっています。そして、AnyVal
のサブタイプではありません。
scala> class Person(name: String, age: Int) // defined class Person scala> val person: Person = null val person: Person = null scala> val num: Int = null 1 |val num: Int = null | ^^^^ | Found: Null | Required: Int
つまり、AnyRef
を継承しているクラス型の変数にはNull型の唯一の値であるnull
を代入できますが、AnyVal
を継承しているInt型の変数にはnull
の代入はできない、ということを示します。
Option型によるnullへの対応
しかし、nullはやっかいです。参照すればすぐにNull pointer exceptionが発生します。意図してインタフェースとしてnullを取り得る設計としたとしても、受け取る側がnullに対応するコードを十分に書けないこともあるし、意図してnullは取り得ない設計としたとしても渡す側がうっかりnullを渡すことが有ります。
前者は、require
などで引数のnullチェックを行うルールにすべきですが、残念ながら強制力がありません。
強制力を持たせるためにはインタフェースの境界を、Option型でラップすることです。
scala> case class Person(name: String, age: Int) // defined case class Person scala> def printPerson(person: Option[Person]): Unit = | person match { | case Some(p) => println(s"name:${p.name}, age: ${p.age}") | case None => println("no person") | } | def printPerson(person: Option[Person]): Unit scala> val p1 = null val p1: Null = null scala> printPerson(Option(p1)) no person scala> val p2 = new Person("Arthur", 42) val p2: Person = Person(Arthur,42) scala> printPerson(Option(p2)) name:Arthur, age: 42
Option型でラップされていることで、Some、Noneに対応するコードを必ず書くための強制力が働き、nullが有る世界と、nullが無い世界を分離されました。
(Optionのapplyにnullを渡すとNoneが生成され、null以外を渡すと、その値をラップしたSomeが生成されます)
世界は平和になりました...
いや、そうか?では全ての外部からもたらされる変数をいちいち全部Option型でラップするのか?意図しないnullの混入を防ぐためにはtoo muchではないか?
EXPLICIT NULLS
nullにおける問題は、その変数がnullを取り得るか、明示されていないことにある...インタフェースの設計として明示に書いてあればいいけど、ドキュメントに頼るのも限界があるし、内部のインタフェースでいちいち文書化もしていられないだろう。
そもそもAnyRefがnullを許容しているからいけない...ということで、Scala3からAnyRef
のサブタイプへのnull代入を不可とするモードが追加になりました。
Explicit Nulls | Scala 3 Language Reference | Scala Documentation
クラス階層が変更になっています。
ただし、デフォルトで有効になるのではなく、コンパイラオプションとして-Yexplicit-nulls
を設定したときだけ有効になります。
$ scala -Yexplicit-nulls scala> case class Person(name: String, age: Int) // defined case class Person scala> val person: Person = null 1 |val person: Person = null | ^^^^ | Found: Null | Required: Person scala> val num: Int = null 1 |val num: Int = null | ^^^^ | Found: Null | Required: Int
もうnullを代入することはできません。
どうしてもNullを代入したい時は、明示的にNull型を許容することを指定します。
scala> val person: Null = null val person: Null = null
しかし、これではNull型に固定されてしまうため、あまり役に立ちません。
Scala3から導入されたUnion型が使えます。
scala> val person: Person | Null = null val person: Person | Null = null
こうして、「うっかり」nullが混入し、Null Pointer Exceptionが発生すること限りなく抑制することができ、かつ記述も簡潔になりました。
世界は少し平和になりました。