この数年で null 安全 1 は一気に浸透したように思います。ざっと思いつくだけでも、次のプログラミング言語で null 安全 が採用されました。
言語 | null 安全 に関する公式の記述 | リプレース対象言語 | オンラインドキュメント | ブラウザ上で試す |
---|---|---|---|---|
Ceylon | Typesafe null and flow-sensitive typing | Java | Tour of Ceylon | Ceylon Web IDE |
Crystal | Null Pointer Exception | Ruby | Crystal Programming Language | play.crystal-lang.org |
Flow | Maybe Types | JavaScript | Quick Reference | Try Flow |
Hack | Nullable | PHP | Hack Documentation | - |
Kotlin | Null Safety | Java | Reference | Try Kotlin |
Python | Union types | - | The Python Tutorial | Interactive Shell |
Rust | Module std::option | C++ | The Rust Programming Language | Rust Playground |
Swift | Optionals | Objective-C | The Swift Programming Language | IBM Swift Sandbox |
TypeScript | Non-nullable Types | JavaScript | Handbook | Playground |
これらは、表中の「リプレース対象言語」に挙げたように、多くのメジャー言語に対する代替手段でもあります。 Java の代わりには Kotlin や Ceylon が、 JavaScript には TypeScript や Flow が、 Objective-C には Swift が、そして PHP には Hack があります。 Python は自身に null 安全 を取り込みました。 Crystal は直接 Ruby と連携して使えるわけではありませんが、 Ruby 風の null 安全 な言語です。 Rust は C++ の代替を目指して開発され、 Firefox の一部で C++ のコードを置き換えるのに使われています 2 。
null
が引き起こしてきた数々の問題を考えると、僕は、 null 安全 は GC (やその他の安全なメモリ管理手法)に匹敵するプログラミング言語の進化だと考えています。僕はこれまで 10 年以上に渡って C や Java, JavaScript, Objective-C, Python などの null 安全 でない 言語を使ってきました。しかし、この 2 年間ちょっとの間、仕事で null 安全 な言語を使うようになって、もはやそれなしのコーディングは考えられないと感じています。もし、まだ null 安全 を体験したことがないなら、それは体験する価値のあるものだと強くオススメします。
null 安全 自体は特に新しい考え方ではありません。しかし、どれだけ優れた仕組みが存在しても、実務で使える言語に採用されなければ多くの人がその恩恵を受けることはできません。 null 安全 が様々な言語に採用されることで、今、ようやく null 安全 な言語を選択肢として考えられる状況になってきました。
かつて、「テストのないコードはレガシーコードだ」 3 という言葉で、テストの必要性が訴えられました。今では、テストが大事だという考えはプログラマの間でほぼ共通認識となっています。同じように、少し気が早くても、あえて null 安全 でない言語はレガシー言語だ と言い切ることで、 null 安全 の普及を加速させることができるんじゃないでしょうか。
本投稿では、 null 安全 の普及を目的とし、 null 安全 について 言語横断的に まとめました。
- null 安全とは何か
- null 安全だと何がうれしいか
- nullable を快適に扱うための仕組み
- nullable の種類と特徴
- 言語別雑感
- null 安全な言語の始め方
- FAQ
null 安全とは何か
null 安全 とは、一言で言えば、 null
( nil
, None
など)が原因で実行時エラーにならない仕組みです。
null
に対してメソッドコールやメンバアクセスすると多くの言語では実行時エラーとなります。
// Java
String s = null;
s.length(); // java.lang.NullPointerException
// JavaScript
let s = null;
s.length; // TypeError: Cannot read property 'length' of null
# Python
s = None
len(s) # TypeError: object of type 'NoneType' has no len()
一方、 null 安全 な言語では実行時ではなくコンパイル時(動的型付言語では静的解析時)にエラーとなります。プログラムが実行できないので、当然ながら null
が原因で実行時エラーになることはありません。実行するためにはコードを修正する必要があります。
// Kotlin
val s: String? = null
s.length // error: only safe (?.) or non-null asserted (!!.) calls are allow
// TypeScript
let s: string|null = null;
s.length; // Object is possibly 'null'.
# Python
s = None # type: Optional[str]
len(s) # error: Argument 1 to "len" has incompatible type "Optional[str]"; expected "Sized"
上記の String?
や string|null
, Optional[str]
が重要な点です。この ?
や |null
, Optional
はこの変数 s
が null
かもしれない( nullable である)ことを表しています。 null
には length
というメンバはないので、 s.length
はコンパイルエラーを起こし、コードを修正するよう要求されているわけです。
では、どうやって s
の長さを取得すればいいのでしょうか。 s
が null
でないことをチェックすれば OK です。
// Kotlin
val s: String? = null
if (s != null) {
s.length // OK
}
// TypeScript
let s: string|null = null;
if (s != null) {
s.length; // OK
}
# Python
s = None # type: Optional[str]
if s is not None:
len(s) # OK
この if
の {}
の中では s
が null
でないことが確定しており、そのため s.length
がエラーになりません。コンパイラが制御フローを解析し、 {}
の中では s
は String?
ではなく String
だと判断してくれるのです。
では、変数宣言時に ?
や |null
, Optional
を付けると null
チェックが必須になるのなら、それらを付けずにいきなり null
を代入してみたらどうなるでしょうか。
// Kotlin
val s: String = null // error: null can not be a value of a non-null type kotlin.String
s.length
// TypeScript
let s: string = null; // Type 'null' is not assignable to type 'string'.
s.length;
# Python
s = None # type: str
# error: Incompatible types in assignment (expression has type None, variable has type "str")
len(s)
この場合は代入がコンパイルエラーになります。 null 安全 な言語では、 ?
や |null
, Optional
が付いていない型( non-null 4 )の変数に null
を代入することはできません。そのため、 non-null な変数の値は null
でないことが保証されています。上記の s.length
も null
が原因で実行時エラーになることはありません。
このように nullable と non-null を型で区別し、 null
による実行時エラーを防止する仕組みが null 安全 です。
null 安全だと何がうれしいか
プログラマはコードを書き、それをコンパイル(または静的解析)し、実行します。実行できるプログラムはテストされ、出荷され、ユーザーが利用します。この工程の中で、 問題は早めに発見されることが好ましい です。もし、実行時エラーをテストで検出できなければ、それはそのまま潜在バグとして製品に組み込まれ、出荷されてしまいます。コンパイルエラーであれば開発時に 必ず 気付くことができます。
経験上、コードのロジック自体が間違えていたというケースを除けば、 null
は最もよくあるバグの原因の一つだと思います。その問題をコンパイル時に排除できるのは素晴らしいことです。
null
の生みの親である Tony Hoare は次のように述べています 5 。
それは10億ドルにも相当する私の誤りだ。null参照を発明したのは1965年のことだった。(中略)これは後に数え切れない過ち、脆弱性、システムクラッシュを引き起こし、過去40年間で10億ドル相当の苦痛と損害を引き起こしたとみられる。
null
の発明から半世紀を経て、ようやく僕らは null 安全 によって null
の苦しみから解放されようとしているのです。
null 安全は生産性も高める
いくら安全になるとはいえ、 null
チェックを強制する null 安全 は一見煩わしく、生産性を下げるものに見えるかもしれません。しかし、 null 安全 は安全性を高めると同時に生産性も高めます。
null 安全 では、 Foo?
などで表される nullable が目立ちますが non-null も重要です。 null
が代入されることのない変数や null
を許容しない引数、 null
を返さない関数の戻り値などはすべて non-null にできます。 null 安全 でない 言語では値が null
でないことを assert
やテストで確認しなければなりません。 non-null を使えば、それらがなくてもコンパイラが null
でないことを保証してくれます。しかも実行時ではなくコンパイル時にです。
nullable に対して書かなければならない null
チェックは、 null 安全 でない 言語でも必要なものです。余分な null
チェック が増えているわけではありません。
つまり、 null 安全 は
- non-null によって不要なコードを省略できる
- nullable によってエラーを早め(コンパイル時)に検出できる
ことによって、安全性だけでなく生産性も高める仕組みなのです。
nullable を快適に扱うための仕組み
null 安全 なコードを快適に書くためには、 nullable を快適に扱える必要があります。基本的に nullable な値は null
チェックによって処理されるわけですが、 null
チェックだけで対処するには面倒なケースもあります。 null 安全 な言語の多くには、そのようなケースで nullable な値を扱うための便利な仕組みが備わっています。本節ではそのような仕組みの内、次のものを取り上げ説明します。
-
null
チェックに伴うキャスト -
!
,!!
-
?.
,?->
-
?:
,??
map
flatMap
-
do
記法 ?
- 型推論
null
チェックが煩わしいと感じるなら、道具を上手く使えていないのかもしれません。本節を読むことでそのような問題が解決する可能性があります。もしくは、あなたの言語に「快適に書くための仕組み」が足りていないのかもしれません。「快適に書くための仕組み」の充実度は言語によって大きく異なります。次の表は、どの言語にどの道具があるかをまとめた表です。「快適に書くための仕組み」の中には関数で実装できるようなものもあるので、他の言語の「仕組み」を眺めて自分の言語に取り入れられないか考えてみて下さい。
| 言語 | !
, !!
| ?.
, ?->
| ?:
, ??
| map
| flatMap
| do
記法 | ?
| 型推論 |
|:--|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| Ceylon | | ◯ | ◯ | | | | ◯ | ◯ |
| Crystal | | | | | | | | ◯ |
| Flow | | | | | | | ◯ | ◯ |
| Hack | | ◯ | | | | | ◯ | ◯ |
| Haskell | ◯ | | ◯ | ◯ | ◯ | ◯ | | ◯ |
| Java | ◯ | | ◯ | ◯ | ◯ | | | △ |
| Kotlin | ◯ | ◯ | ◯ | ◯ | ◯ | | ◯ | ◯ |
| Python | | | | | | | △ | ◯ |
| Rust | ◯ | | ◯ | ◯ | ◯ | | | ◯ |
| Scala | ◯ | | ◯ | ◯ | ◯ | ◯ | | ◯ |
| Swift | ◯ | ◯ | ◯ | ◯ | ◯ | | ◯ | ◯ |
| TypeScript | ◯ | | | | | | | ◯ |
- Java と Scala は null 安全 な言語ではありませんが、
null
を使わずにOptional
,Option
を使って null 安全 なコードを書くことができます。そのため、Optional
,Option
でどの道具を使えるかを示すために表に加えています。 - Python の
?
が △ なのは、Union[Foo, None]
をOptional[Foo]
と表すシンタックスシュガーはあるけれども、Foo?
ほど簡潔でないからです。 - Java の型推論が △ なのは、ラムダ式の型は推論できるけれども変数の型は推論できないからです。
null
チェックに伴うキャスト
これまでに見てきたように、 if
文で null
チェックをすると、その if
文の {}
の中では String?
が String
として扱われる(キャストされる)という仕組みがありました。この仕組みは スマートキャスト (Smart cast) (Kotlin), 制御フロー解析 (Control flow analysis) (TypeScript), Flow-sensitive typing (Ceylon) などと呼ばれています。以下、本投稿では スマートキャスト と呼びます。
スマートキャスト がないと、 null
チェックをしても s
は String?
のままです。 s
が String?
のままだと、 null
チェックをしても結局 s.length
でコンパイルエラーになってしまいます。 length
にアクセスするには s
を強制的に String
にキャストしなければなりません。
// Kotlin
val s: String? = null
if (s != null) {
(s as String).length // String にキャストしてから s を使う
}
しかし、これだともし null
チェックを忘れてもコンパイルが通ってしまいます。
// Kotlin
val s: String? = null
(s as String).length // 実行時エラー
s
は null
なので、上記のコードは String
にキャストしようとしたときに実行時エラーになります。 nullable と non-null によって、せっかくコンパイル時に null
によるエラーを検出できるようになったのに、これでは意味がありません。
このように、 null
チェックと non-null へのキャストが独立していると、キャストの失敗によるエラーが実行時に発生します。 null
チェックと non-null へのキャストをセットで行う仕組みは null 安全 を実現する上で欠かせまぜん。
似たような仕組みとして、 Swift や Rust には if let
(Optional binding) が、 Java の Optional
には ifPresent
メソッドがあります。 Haskell や Scala では パターンマッチング を使います。どれも スマートキャスト とは異なる仕組みですが、やりたいことは null
チェックの条件分岐と non-null へのキャストという意味では同じです。
!
, !!
どうしても null
である可能性を無視して nullable を non-null に変換したいこともあります。
例として、テキストフィールドに入力された文字列を整数に変換する処理を考えましょう。一般的に、文字列から整数への変換は失敗する可能性があります。 "123"
は 123
に変換できますが "ABC"
は整数に変換できません。変換に失敗した場合には null
を返すとすると、文字列を整数に変換する処理は String
から Int?
への変換だと考えられます。
// Swift
let number: Int? = Int(text)
しかし、テキストフィールドに制約が加えられていて数字しか入力できないこともあります。そのようなケースでは文字列から整数への変換に失敗することはありません。それでも、テキストフィールドから得られるのは文字列なので、型の上では整数への変換が必要となります。
// Swift
if let number: Int = number { // null チェック
// ここでは number を Int として使える
}
必ず null
でないとわかっている場合にも、 null
チェックを書かなければならないのは手間です。 型は万能ではありません。ロジックの上では null
にならないけれど、型の上では nullable になってしまうことがしばしばあります。このままでは、 null 安全 でない 言語では書かなくて良かった無駄な null
チェックを書かなくてはならないことになり、生産性が下がりかねません。 null
である可能性を明示的に無視して nullable を non-null に変換する簡単な方法が必要です。
Swift では nullable な値の後ろに !
という後置演算子を付けることで、強制的に non-null に変換することができます。これを Forced unwrapping と呼びます。
// Swift
let number: Int = Int(text)!
number
の型が Int?
ではなく Int
であることに注意して下さい。これは、 !
によって Int?
型である Int(text)
の戻り値が Int
型に強制変換されたからです。
もし、値が null
のときに !
で non-null に変換しようとすると実行時エラーとなります。 !
を乱用してはいけません。上記の例のように 絶対に null
にならないけれども型の上では nullable になってしまうケースや、使い捨てのスクリプトで null
ならクラッシュしても良いケースなど、 !
が問題を引き起こさないよう常に意識的でなければなりません。
安全でない操作には長い名前を付けるべきだという意見もありますが、僕はその意見には反対です。補完があれば長くても簡単に入力できます。そして、 !
はたった 1 文字ですが、プログラマが明示的に書く必要があります。 !
を書くときに立ち止まって、本当に問題ないケースか確認するという作業を怠らなければ null 安全 を破壊することはありません。
TypeScript の !
や Kotlin の !!
も同じように nullable を non-null 化する後置演算子です。 Java の Optional
クラスや Scala の Option
クラスには同じ目的の get
メソッドが、 Rust の Option
には unwrap
メソッドがあります。それらを適切に使うことで、快適に null 安全 なコードを書くことができます。
余談ですが、 Swift では Forced unwrapping 以外にも !
という記号は危険な処理に用いられることが一貫されており、 !
を書くときに常に意識的でいやすいです。対して、例えば Java では get
というメソッドが安全な処理( Map
の get
メソッド等)でも使われており、 Swift と比較すると危険な処理であることを意識しづらいのではないかと思います。ロジカルには二つのやっていることは同じですが、視覚的・感覚的に訴えることのできる Swift のような言語デザインは素晴らしいと思います。
?.
, ?->
nullable な値のメンバやメソッドにアクセスしたい、ただし、値が null
の場合は結果も null
でいいというのもよくあるケースです。愚直に書くと次のようになります 6 。
// Kotlin
val s: String? = null
val length: Int?
if (s != null) {
length = s.length
} else {
length = null
}
length
を取得するためだけにこの分岐を書くのは大変です。 Swift や Kotlin, Ceylon, Hack などの ?.
( Swift では Optional chaining と呼びます)を使えばこれを簡単に実現できます( Hack では .
の代わりに ->
を使うので ?->
です)。
// Kotlin
val s: String? = null
val length: Int? = s?.length // s が null なら結果も null
特に、 ?.
は null
になるかもしれない操作をチェーンするときに重宝します。
// Kotlin
val baz: Baz? = foo?.bar?.baz
これを null
チェックと条件分岐で書こうとすると次のようになります。
// Kotlin
val baz: Baz?
if (foo != null) {
val bar: Bar? = foo.bar
if (bar != null) {
baz = bar.baz
} else {
baz = null
}
} else {
baz = null
}
?.
によって、 null
チェックによる複雑な条件分岐を書かかずに簡潔なコードで済ませられます。
?:
, ??
null
の場合にデフォルト値を与えたいというのもよくあるケースです。
// Kotlin
val s: String? = null
val length: Int
if (s != null) {
length = s.length
} else {
length = 0 // デフォルト値 0 を与える
}
Kotlin の ?:
演算子や Swift の ??
演算子を使えばこれを簡単に書けます。
// Kotlin
val s: String? = null
val length: Int = s?.length ?: 0 // デフォルト値 0 を与える
Java の Optional
の orElse
メソッドも同様の働きをします。
map
?.
は強力な構文ですが、 nullable な値を演算子のオペランドにしたり、関数やメソッドの引数に渡したいときには役に立ちません。
たとえば、 Int?
を二乗することを考えてみましょう。 ?.
と同じように元の値が null
のときは結果として null
を得たいとします。
もし、 square
(二乗する)というメソッドがあれば次のように書けます。
// Swift
let number: Int? = nil
let squared: Int? = number?.square()
しかし *
演算子で number * number
のように書こうとすると ?.
ではうまく書けません。
// Swift
let number: Int? = nil
let squared: Int? = number?. * number?. // コンパイルエラー
これを null
チェックと条件分岐で書くと次のようになります。
// Swift
let number: Int? = nil
let squared: Int?
if let number: Int = number { // null (nil) チェック
squared = number * number
} else {
squared = null
}
とても面倒です。こういう場合には、 map
という高階メソッドが役立ちます。
// Swift
let number: Int? = nil
let squared: Int? = number.map { $0 * $0 }
Swift を知らないと number.map { $0 * $0 }
という表記が何を表しているのかわかりづらいと思いますが、これは JavaScript の number.map(function(x) { return x * x; })
または number.map(x => x * x)
、 Java の number.map(x -> x * x)
に相当します。
map
メソッドに渡されたラムダ式は、 Optional
の値が nil
でない場合だけ実行され、 map
の結果として返されます。ラムダ式の引数に渡されるのは non-null 化された値です。もし Optional
の値が nil
であれば結果も nil
になります。
Java の Optional
や Scala, Rust の Option
の map
メソッド、 Haskell の Maybe
の fmap
も同様の挙動をします。 Kotlin では nullable に map
メソッドはないですが、 ?.
と let
メソッドを組み合わせることで number?.let { it * it }
と書くことができます。
flatMap
前述の文字列から整数への変換
// Swift
let number: Int? = Int(text)
の `text` が `String` ではなく `String?`だったらどうでしょうか。整数への変換が `Int?` を返すので、 `map` で書くと `Optional` がネストして戻り値の型が `Int??` になってしまいます。
```swift
// Swift
let text: String? = nil
let number: Int?? = text.map { Int($0) }
Int??
がネストしている部分です。今ほしいのは Int?
なのでこのままではダメです。
flatMap
は map
と似ていますが、ネストした Optional
を一重に潰して(フラットにして)返してくれます。
// Swift
let text: String? = null
let number: Int? = text.flatMap { Int($0) }
Java の Optional
や Scala の Option
の flatMap
メソッド、 Rust の Option
の and_then
、 Haskell の Maybe
の >>=
演算子も同様の挙動をします。
実は map
と flatMap
があれば ?.
でできることはすべてできます。 ?.
は map
と flatMap
をより簡単に書くための表記だと言えるでしょう。 map
と flatMap
は ?.
ではカバーできないケースに対応可能なより柔軟な仕組みとして重要です。
なお、 Kotlin や Ceylon, TypeScript, Python では nullable をネストすることができません。詳しくは後述しますが、たとえば Kotlin の Int??
は Int?
と等価になります。そのため、これらの言語では map
と flatMap
が等価になります。 Kotlin では flatMap
相当のことをするのにも、 map
と同じく ?.let
で代替できます。
do
記法
a: Int?
と b: Int?
があったときに、 a
と b
を足すにはどうすればいいでしょうか。例によって、 a
か b
のどちらかが null
だった場合は結果として null
を得たいとします。
null
チェックをして条件分岐は面倒なので、 flatMap
と map
を使って書いてみます。
// Swift
let a: Int? = 2
let b: Int? = 3
let sum: Int? = a.flatMap { x in b.map { y in x + y } }
a.flatMap { x in b.map { y in x + y } }
は、 JavaScript なら a.flatMap(x => b.map(y => x + y))
、 Java なら a.flatMap(x -> b.map(y -> x + y))
という意味です。
書けないことはないですが、ラムダ式をネストするのは少々読みづらいです。 Haskell では do
記法を使い、より簡潔に記述できます。
do
記法は flatMap
のネストを簡潔に書くための記法です。 map
が混ざっていてはいけないので、まずは map
を flatMap
で書き変えてみましょう。
// Swift
let sum: Int? = a.flatMap { x in b.flatMap { y in .some(x + y) } }
.some(x + y)
は、 Int
型である x + y
の結果を Optional
で包んで Int?
型にするための処理です。これを Haskell で書くと次のようになります。
-- Haskell
sum = a >>= (\x -> b >>= (\y -> Just (x + y)))
>>=
は Swift の flatMap
に、 Just
は .some
に当たります。 do
記法を使うと、このコードを次のように書き変えられます。 do
記法は >>=
のシンタックスシュガーなので、この二つのコードは等価です。
-- Haskell
sum = do
x <- a
y <- b
return (x + y)
<-
は do
記法で用いられ、上記のコードでは nullable な a
, b
を non-null な x
, y
に変換しています。 non-null に変換さえできれば最後に x + y
を計算するだけです。 >>=
( flatMap
)をネストするよりずっとわかりやすいと思います。
Scala にも同様の仕組みがあります。
// Scala
val a = Option(2)
val b = Option(3)
val sum = for {
x <- a
y <- b
} yield x + y
これまでの関係をまとめると、
- 多くのケースで簡潔な
?.
を使う - 一部のケースでは
?.
では書けないので、上位互換のmap
やflatMap
を使う -
flatMap
がネストするとわかりづらいのでdo
記法を使う
となります。ただし、これは言語横断的な話で、 Haskell や Scala に ?.
はないですし、 Swift や Kotlin には do
記法がありません。実用上はその言語で欠けている部分を諦めて別の方法で妥協する必要があります。
?
Swift や Kotlin, Ceylon, Hack, Flow などの言語では、型名に ?
を付けて nullable な型を表します。 nullable な型を書く機会は多いので、簡潔な記法が用意されているのはうれしいところです。
Foo?
は、 Swift では Optional<Foo>
の、 Ceylon では Foo|Null
のシンタックスシュガーです。 Kotlin のように ?
が直接的に nullable であることを表す言語仕様なら ?
の存在は必須ですが、そうでなくても ?
がシンタックスシュガーとして用意されていれば便利です。
TypeScript には、残念ながら 2.0 時点でこのようなシンタックスシュガーはありません。 Crystal にもありませんが、 Crystal のように極力コード中に型を書かず推論させる言語であれば不要だと思います。
型推論
直接的に null 安全 に関わっているわけではないですが、僕は 型推論 も null 安全 を快適にするために重要だと考えています。
null 安全 は、 null
に関するエラーを型を使ってコンパイル時(または静的解析時)に検出する仕組みです。そのため、言語に静的型付けであることを強制します。しかし、 null 安全 のために動的型付けの書き心地が損なわれてしまっては、 null 安全 を取るのか、動的型付けの書き心地を取るのかという話になってしまいます。
静的型付けでも、 型推論 で変数や引数、戻り値の型を書かなくてよいのであれば、動的型付けに近い書き心地を実現できます。
// Swift (型推論なし)
let numbers: [Int] = [2, 3, 5]
let squares: [Int] = numbers.map { (x: Int) -> Int in x * x }
// Swift (型推論あり)
let numbers = [2, 3, 5]
let squares = numbers.map { $0 * $0 }
// JavaScript
let numbers = [2, 3, 5];
let squares = numbers.map(x => x * x);
静的型付けの Swift でも型推論があれば、動的型付けの JavaScript と同じような簡潔さでコードが書けているのがわかると思います。
nullable の種類と特徴
一口に nullable と言ってもその実現方法は様々です。しかし、大きく分けると二つに分けられます。それは nullable な型をネストできるかできないかです。
ネストできる
- Haskell
- Java
- Rust
- Scala
- Swift
ネストできない
- Ceylon
- Crystal
- Hack
- Kotlin
- Python
- TypeScript
nullable な型をネストした Foo??
という型は何を表すのでしょうか。もし Foo?
を単に Foo
か null
を入れられる型だと考えると、 Foo??
であっても null
を入れられるという性質を二重に記述しているだけになり Foo?
と変わらないように思えます。
Foo??
が何の役に立つのかは例を見てみるのが一番です。最も身近な例の一つは、 Map
や Dictionary
、ハッシュを使うときです。
Kotlin や Java の Map<K, V>
は K
型のキーで V
型の値を引くためのクラスです。キーで値を引くと、キーに対応した値が存在する場合としない場合があります。キーが存在しない場合は null
を返したいので、戻り値の型は V?
になります。例えば、 Map<String, Int>
なら String
型のキーで値を引き、戻り値として Int?
を得ます。
// Kotlin
val map: Map<String, Int> = mapOf("a" to 2, "b" to 3, "d" to 5)
map["a"] // 2
map["e"] // null (キー "e" に対応した値が存在しない)
では、値として null
も格納したい場合はどうなるでしょうか。つまり、 Int?
を V
とし、 Map<String, Int?>
としたいということです。このとき、キーで値を引いた戻り値の型は、 V?
の V
が Int?
なので Int??
になります。しかし、 Kotlin では ?
は null
を代入できるかどうかを表すだけなので、 Int??
は Int
か null
か null
か、という意味になり Int?
と同一です。その結果、
// Kotlin
val map: Map<String, Int?> = mapOf("a" to 2, "b" to 3, "c" to null, "d" to 5)
map["a"] // 2
map["c"] // null (キー "c" に対応した値 null )
map["e"] // null (キー "e" に対応した値が存在しない)
となり、 null
という値が返されたのか、値が存在しなかったのかを区別することができません。
もし nullable な型をネストできれば話は違います。 Swift では Int??
と Int?
が区別され、次のようになります。
// Swift
let map: [String: Int?] = ["a": 2, "b": 3, "c": nil, "d": 5]
map["a"] // Optional(Optional(2))
map["c"] // Optional(nil)
map["e"] // nil
これなら、 nil
という値が存在したのか、それとも値が存在しなかったのかで分岐することができます。
// Swift
if let c: Int? = map["c"] {
if let c: Int = c {
// nil でない値が存在した場合
} else {
// 値 nil が存在した場合
}
} else {
// キーに対応した値が存在しなかった場合
}
nullable な型がネストできるということはそれだけ表現力が高いということです。ですので、僕は一般論としてはネストできる方が望ましいと考えています。ただ、パフォーマンスや言語仕様、実行環境との相性などを考慮すると、何がベストかは一概には言えません。たとえ nullable な型をネストすることができなくても null 安全 の魅力を大きく損ねるわけではありません。たまに不便なケースがある程度です。
本節ではさらに詳しく、それぞれの言語で nullable な型がどのように実現されているのか見ていきます。仕組みを知れば、なぜ nullable な型をネストできる言語とできない言語があるのかも理解できると思います。
nullable か non-null かだけを区別
一番単純な方法で、型に ?
をつけると nullable に、そうでなければ non-null になるという方法です。 Kotlin や Hack が採用しています。ある型 Foo
に ?
をつけて Foo?
とすると( Hack では ?Foo
)、 Foo?
型の変数には Foo
インスタンスの他に null
も代入できます。
// Kotlin
val s1: String = null // コンパイルエラー
val s2: String? = null // OK
この場合、 Foo??
のように nullable な型をネストしても、 Foo
か null
か null
ということを表すだけであり、 Foo
か null
、つまり Foo?
と言っているのと同じです。そのため、 Foo??
と Foo?
は同じ型を表します。
nullable な型をネストできないのは不便なときもありますが、 Kotlin においては妥当な選択だったんじゃないかと思います。 Kotlin は "100% interoperable with Java" を謳う Java 仮想マシン上で動作する言語です。 Java には null
が存在し、その Java との連携を考えると、この実装は自然で理にかなっていると思います。
Union type
Ceylon や TypeScript, Python, Crystal が採用しているのがこの方式です。これらの言語の型システムには Union type が存在しており、それを使って nullable を実現しています。 Union Type とは複数の型のいずれかを表す型です。たとえば、 Foo|Bar
のようにして Foo
または Bar
を代入可能な型を作ります( Python では Union[Foo, Bar]
)。
Ceylon では Foo|Null
, TypeScript では Foo|null
, Python では Union[Foo, None]
のようにして nullable な型を表します。 Ceylon の null
は Null
型の値、 TypeScript の null
は null
型の値、 Python の None
は None
型の値として扱われます。
// Ceylon
Foo|Null foo = null;
// TypeScript
let foo: Foo|null = null;
# Python
foo = None # type: Union[Foo, None]
Foo|Null
が Foo
型の値か null
かを代入できるというのは自然な表現です。言語仕様として Union type を持つなら、 Union type で nullable を表現するのは自然だと思います。
なお、 Ceylon や Python には、 Foo|Null
や Union[Foo, None]
をより簡潔に記述するためのシンタックスシュガーが用意されています。 Ceylon では Foo?
は Foo|Null
と、 Python では Optional[Foo]
は Union[Foo, None]
と等価です。上記のコードは次のようにも書けます。
// Ceylon
Foo? foo = null;
# Python
foo = None # type: Optional[Foo]
TypeScript にはこのようなシンタックスシュガーはないので単純に string|null
のようにして nullable であることを表します。 Crystal にもシンタックスシュガーがないですが、前述のように、 Crystal では型を明記することがほとんどないので問題にならないと思います。
Union type で nullable を表す場合もネストすることはできません。 Kotlin のときと同じで、 Foo|Null|Null
(Foo??
) は Foo
か Null
か Null
かなので、 Foo|Null
(Foo?
) 、つまり Foo
か Null
かと同一です。式変換で書くと Foo?? == (Foo?)? == (Foo|Null)|Null == Foo|Null|Null == Foo|Null == Foo?
と書けます。
Tagged union
Swift や Rust, Haskell が採用しているのがこの方法です。 Swift でも Foo?
のようにして nullable な型を表しますが、値を持つときは some
、持たないときは none
というタグが付けられています。 Union type 風に書くと、 Foo?
が some(Foo)|none
と表されるようなイメージです。タグ付きの Union なので Tagged union と呼ばれます。
nullable を表す Swift の Optional
と Haskell の Maybe
は次のように宣言されています。
// Swift
enum Optional<T> {
case none
case some(T)
}
-- haskell
data Maybe a = Nothing | Just a
Swift では none
と some
が、 Haskell では Nothing
と Just
がタグです。
タグがあることで nullable な型をネストできるようになります。 Foo?? == (Foo?)? == some(some(Foo)|none)|none
なので、明確に外側の ?
が none
なのか、内側の ?
が none
なのかを区別することができます。
Union type 同様、 Tagged union で nullable な型を表すのも、直接的に値があるかないかを表しているので自然です。言語仕様として Tagged union を持つなら、ネストでき、自然な表現である Tagged union で nullable な型を表すのが望ましいと思います。
クラスや構造体
Java の Optional
や Scala の Option
はクラスで実現されています。
Java の Optional
は単純にフィールドに値を保持し、それが null
かどうかでメソッドの挙動が変化するようになっています。 Scala はおもしろくて、抽象クラス Option
を継承した具象クラス Some
と None
として実装され、ポリモーフィズムで挙動が切り替わるようになっています。オブジェクト指向的には Scala の方がキレイな実装なように感じます。
Java の Optional
の isPresent
メソッドは次のように実装されています。
// Java
public boolean isPresent() {
return value != null;
}
しかし、 Scala の Option
の isEmpty
は Some
と None
でそれぞれ実装されています。
// Scala
final case class Some[+A](x: A) extends Option[A] {
def isEmpty = false
}
case object None extends Option[Nothing] {
def isEmpty = true
}
false
, true
を返しているだけで条件分岐はありません。ただし、 Scala がポリモーフィズムで切り分けているのは isEmpty
だけです。 map
や flatMap
など、その他のメソッドは条件分岐で記述されています 7 。やろうと思えばすべてのメソッドの挙動をポリモーフィズムで切り替えられます 8 が、おそらくパフォーマンス上の(条件分岐があっても静的ディスパッチした方が速いという)理由で条件分岐で書かれているのだと思います。
なお、 Java の Optional
を Scala の Option
のように継承でサブクラスを作れなかったのは、 Java に sealed class
がないからだと思われます。もし、 Java で Optional
を継承して Some
と None
を作ろうとすると、 Optional
を final
クラスにすることができないので Some
と None
の他にも Optional
のサブクラスを作れてしまいます。 Scala の sealed class
は継承を限定できるので Some
と None
以外に Option
のサブクラスを作ることはできません。
この方法であれば、 Optional
はただのジェネリックなクラスなので Optional<Optional<Foo>>
として問題なくネストできます。
追記: コメントで @kmizu さんにご指摘いただいた通り、 Scala や Kotlin の sealed class
は Tagged union を作る手段なので、 Scala の Option
は Tagged union とも言えます。詳しくはコメント欄を御覧下さい。
言語別雑感
今度は言語別に null 安全 に関連した雑感を述べます。
- C#
- C++
- Crystal
- Go
- Java
- JavaScript
- Kotlin
- Objective-C
- Python
- Ruby
- Scala
- Swift
- TypeScript
C#
C# の T?
(Nullable<T>
) は本投稿で見てきた nullable とよく似ているように見えますが、 T
として値型しかとれないという問題があります。また、 nullable な値のメンバにアクセスしてもコンパイルエラーとなりません。これが参照型に拡張され、静的に型チェックされ、参照型の T
が non-null 化されれば null 安全 な言語となります。
現状では中途半端で残念な仕様ですが、 C# はこれまでも先進的な仕様を取り入れてきた言語であり、また null 安全 になった TypeScript と同じ開発者が関わっているので、今後 null 安全 になることを期待しています。
追記: @gorn708 さんから補足コメントをいただきました。
次のC#のバージョンだとこの辺が本件の関連ですかね。
https://github.com/dotnet/roslyn/blob/features/NullableReferenceTypes/docs/features/NullableReferenceTypes/Nullable%20reference%20types.md
この Proposal が採用されて、 C# が null 安全 に一歩近づくといいですね。
C++
C++17 で std::optional
が導入されます。しかし、それだけで null 安全 になるわけではありません。既存のコードベースが巨大なので、すべてが std::optional
に対応して、 C++ が null 安全 になるというのは現実的ではないでしょう。そもそも、そういうことを目指している言語でもないと思うので、それでいいのかもしれませんが。
Crystal
基本的に型を書かずに推論させるという方針は Ruby 的な言語仕様と相性が良さそうです。しかし、 Ruby と連携せずにどこまで Ruby プログラマを取り込めるかが普及するかの分かれ目になってきそうです。まだ 1.0 がリリースされていないので、今後の展開に注目です。
Go
Go は過去 10 年で登場した人気言語の中ではめずらしく null 安全 でない 言語です。
僕は Go に詳しくないですが、 Go はこれまでのプログラミング言語の進化に逆らって、大胆に仕様を削ぎ落としてシンプルであることを目指した言語だと理解してます。ジェネリクスがなくても型付けされた配列やマップがあれば大体問題ないでしょ的な。
しかし、 Kotlin のように単に nullable と non-null を区別するだけであれば、言語仕様も大して複雑化しないと思うので、是非取り入れて欲しいところです。身近に Go に精通した人がいないので、 Go プログラマがどう感じているのか聞いてみたいです。
Java
Java 8 で Optional
が追加されましたが、 Java で nullable をすべて Optional
にしようというのは無理があります。標準ライブラリレベルで null
を返すものがたくさんあるので、結局混ざってしまいます。例えば、 Map
の get
メソッドはキーに対応する値がなければ null
を返します。 null 安全 な言語では nullable になっているところです。また、 Java の Optional
はフィールドに使わない方がいい 9 という話もあり、そもそもが null を代替できるものではありません。
別のアプローチとして、 JSR 305 10 が進めばアノテーションで non-null チェックできるようになるかもしれません。標準ライブラリに片っ端から @NonNull
をつけてくれれば大分楽になりそうです。ただ、しばらくは JSR 305 の標準化も期待できないので、サードパーティの @NonNull
的なものでがんばるしかないのでしょうか。たとえ @NonNull
が標準になっても、本投稿で挙げた「快適に書くための仕組み」があるわけではないですし、辛い状況は続きそうです。
JavaScript
型アノテーションに関する話もある 11 12 13 ようですが、近いうちに採用される見込みはほぼなさそうです。 null 安全 は更にその先の話になるので、 JavaScript を null 安全 に書きたいなら、しばらくは TypeScript か Flow を使うのが現実的だと思います。
Kotlin
言語自体の問題ではないですが、 Swift が Objective-C の言語仕様に手を入れることで Objective-C との連携を改善して、 Objective-C との境界でも null 安全 なコードを書けるようになってきているのに対して、 Kotlin と Java の境界では null 安全 が破壊されてしまう状況が辛いです。このあたりは、同じ組織が両言語を開発しているかどうかの差が大きそうです。前述のように Java が @NonNull
を採用してくれれば解決するかもしれません。
Objective-C
Objective-C はめずらしく、 nil
に対してメソッドコールしたり、 nil
のプロパティにアクセスしたりしてもエラーにはならない言語です。
Foo *foo = nil;
NSString *bar = foo.bar; // OK: bar == nil
しかし、これは null 安全 ということではありません。いつでも nil
が実行時エラーを起こさないわけではなく、この bar
を次のように使うと実行時エラーとなります。
NSURL *url = [[NSURL alloc] initWithString: bar]; // 'NSInvalidArgumentException', reason: '*** -[NSURL initWithString:relativeToURL:]: nil string parameter'
Objective-C の nil
の扱いは一見安全なように見えて、問題の発覚を遅らせ、解決をより難しくしているだけです。潜在バグを生みやすいという点で、僕は Objective-C 方式は最悪だと考えています。かつて、 Objective-C で書いたプログラムを Java に移植したときに、 Java で NullPointerException
が発生して初めて Objective-C 側の nil
ハンドリングが不適切だったことに気付くということが何度もありました。
Swift が安定してきた今、もはや Swift を使わずに Objective-C を使う理由はほぼないように思います。 C++ 連携したいのでもない限り Swift を使いましょう。
Python
Python の言語仕様に型ヒント 14 が取り入れられるのは 3.5 からですが、コメント構文を使うことで Python 2 でも型ヒントを記述し null 安全 なコードを書くことができます。
# Python 3
def to_int(s: str) -> Optional[int]:
# Python 2
def to_int(s): # type: (str) -> Optional[int]
型ヒント やコメント構文で記述された型は mypy 15 16 を使ってチェックします。 mypy は Python の静的型チェッカーです。 Python 2 でも使える 17 ということは、すべての Python プログラマが今すぐでも null 安全 なコードを書けるということです。
なお、 mypy は現状ではデフォルトで null 安全 なモードになっていないので、 null 安全 にするには
mypy --strict-optional foo.py
のように --strict-optional
オプションを指定する必要があります 18 。
Python の Optional
にはまだ「快適に書くための仕組み」のほとんどありません。関連する Proposal として PEP 505 19 がありますが、当面は「快適な仕組み」をあきらめるか、自力で関数を作って再現する必要があるでしょう。
Ruby
静的型付 + 型推論が流行り、多くの動的型付言語も型アノテーションを取り入れる中、一向にその動きが見えなかった Ruby ですが、 RubyKaigi 2016 の Matz の講演 20 で静的に型推論する話が取り上げられました。これがどういうものになるかはまだわかりませんが、 null 安全 のためには静的型が欠かせないので歓迎です。もし、 Ruby 3 に null 安全 も取り入れられればうれしいですね。
Scala
僕は Scala をほとんど触ったことがないので、 Scala で null
がどのように扱われているのか長い間疑問だったんですが、次のツイートで疑問が解けました。
たとえば、「ScalaはnullをOptionで代替できるけど、言語仕様上はnullを使えるから結局その点はJavaと変わらない」という批判がたまにあるのだけど、純粋に言語仕様に関する批判としては正しいのだけど、「Scalaではnullを(原則)使わない」文化が醸成されていて
— 水島宏太(構文解析おじさん) (@kmizu) 2016年10月1日
Scala は言語仕様的には null 安全 でないけれども、 null
を使わずにすべて Option
に統一するという文化によって null 安全 を実現しているようです。
Swift
ここに挙げた言語の中では nullable な型を一番快適に使える言語だと思います。「快適に書くための仕組み」が充実していることに加え、連携先の言語( Swift の場合は Objective-C )側を修正することで、 null 安全 でない Objective-C との境界でも null 安全 なコードを書けるようになっていることが大きいです。両方の言語を Apple が主導しているからこそできる芸当です。
TypeScript
TypeScript 2.0 で null 安全 な言語に進化を遂げました。ただし、今のところデフォルトでは null 安全 なモードになって いない ので、 --strictNullChecks
フラグをオンにする必要があります。 6
null 安全 になったとは言うものの、今のところ Python 同様「快適に書くための仕組み」がほぼありません。例えば、 ?.
は JavaScript 待ちのようです 21 。将来的な発展に期待です。
null 安全な言語の始め方
null 安全 な言語が良さそうに見えても、既存のコードベースやライブラリもあり、今使っている言語を変更するというのは大変な作業です。しかし、 null 安全 な言語の中には既存の言語とシームレスに連携して、一つのプロジェクトの中でミックスしてコードを書けるものもあります。それなら、新しく書く部分だけを null 安全 な言語で書き始めることもできます。
本節では、次の null 安全 でない 言語を使っている人を対象に、できるだけ簡単に null 安全 な言語を使い始めるための方法を述べます。
言語 | null 安全 な言語の導入しやすさ |
---|---|
Java | ◯ |
JavaScript | ◎ |
Objective-C | ◯ |
Python | ◎ |
Ruby | △ |
ただし、マークの意味は次の通りです。
マーク | 意味 |
---|---|
◎ | 既存コードに対して付加的であり、問題が起こってもすぐに元に戻すことができる。導入のための障害はほぼ存在しない。 |
◯ | 既存コードとシームレスに連携することができる。ただし、新しい言語で書いたコードは新しい言語としてメンテする必要はある。新しい言語は言語仕様が変化しがちなので、メンテコストは既存コードより相対的に高くなることが多い。 |
△ | 既存のコードと連携することはできない。使い捨てのスクリプトや試験的なプロジェクトなど利用できるケースが限定的。 |
Java
Java の既存コードと連携できて null 安全 なコードを書ける言語としては Ceylon, Kotlin, Scala あたりが有名です。僕は特に Kotlin をオススメします。
Kotlin は構文が Java と近く、 Java プログラマにとって違和感がなく使えると思います。 Ceylon はキーワードが特殊なのと、これからあまり流行ると思えないので微妙です。 Scala については関数型言語的な考え方など学ばなければならないことが多いと思います。
Kotlin と Java では、それぞれの言語で書かれたクラスやメソッドを互いにシームレスに利用することが出来ます。 Java で書かれたクラスをそのまま Kotlin 側で使ったり継承したりできるので、まずは新しく書くクラスだけ Kotlin で書いてみるのがいいと思います。
特に、 Android アプリ開発者であれば、 Kotlin は準公式言語のようなものです。 Android Studio は IntelliJ IDEA をベースに作られていますが、 Kotlin は IntelliJ IDEA を開発している JetBrains が作った言語です。 Android Studio 上でも Java と比べても遜色ないレベルのサポートを受けることができます。 22 23
JavaScript
AltJS は色々ありますが、無難に TypeScript か Flow を使うのがいいと思います。 TypeScript と Flow の使い分けは、 @mizchi さんの↓が参考になります 24 。
流行っているライブラリのみを組み合わせて使う場合や、バックエンドとの連携において型が十分に提供される環境なら、正直、flowtypeよりtypescriptでいいと思っています。flowtypeが力を発揮する環境は、既存のJSが大量に存在する環境や、railsなどの動的な型のフレームワーク環境で、静的な定義が抽出できない環境だと思います。
TypeScript も Flow も JavaScript にトランスパイルされるので、導入後何か問題が起こっても TypeScript や Flow のコードを捨てて、トランスパイル後の JavaScript コードだけ使うことができます。そのため、既存のプロジェクトに対しても導入のハードルは低いです。
なお、 TypeScript 2.0 の段階ではデフォルトで null 安全 なモードになっていないので注意して下さい。 null 安全 なモードにするには、 --strictNullChecks
を指定する必要があります。デフォルトではないですが、常に --strictNullChecks
をオンにすることが推奨されています。 6
Objective-C
Objective-C と連携できる null 安全 な言語は Swift 一択だと思います。これまでの Swift は破壊的変更が多く、メンテコストがおそろしくて Swift に移行できなかった人も多いかもしれません。しかし、現行の Swift 3 からはできるだけ後方互換が保たれる予定です。僕の勤務先では Swift バージョンアップ時のマイグレーションコストを差し引いても Swift で開発する生産性が勝つという判断でこれまでも Swift で開発してきましたが、今後はマイグレーションコストも小さくなり、ますます Objective-C を使う理由がなくなりそうです。
Swift のコードは Objective-C のコードと互いに連携することができます。 Objective-C で書かれたクラスを Swift 側で継承し、それを Objective-C 側で使うようなこともできます。すでに Objective-C のコードが存在するなら、新しく書く部分だけを Swift で書いてみるのがいいと思います。 Xcode を使っているなら、特に新しくインストールしなければならないものはありません。
Python
mypy 15 を導入することで今すぐにでも Python で null 安全 なコードを書くことができます。 Python 2 でも使える 17 ので、今すぐ Python 3 に移行できなくても今すぐ始めることができます。 Python 2 でも使えるコメント構文は実行時にはただのコメントとして解釈され、動作に一切影響を与えないので、既存のプロジェクトにも気軽に導入できます。
mypy と Python の Gradual Typing について学ぶには次の各記事が参考になります。
- [翻訳] Python の静的型、すごい mypy! 16
- What is Gradual Typing: 漸進的型付けとは何か 25
- Mypy syntax cheat sheet 26
- PEP 484 -- Type Hints 14
前述のように、 mypy では現状デフォルトで null 安全 なモードになっていないので、 --strict-optional
オプションを指定する必要があります 18 。
Ruby
残念ながら、本節で挙げた他の言語ほど、 Ruby の場合はうまくいきません。 Ruby の代替である Crystal は、 Ruby と構文が似ているだけで Ruby のコードと連携することができません。既存のプロジェクトの一部だけを Crystal にするというのは難しいでしょう。
しかし、独立したスクリプトなら Crystal で書いても問題ないはずです。書けるところから Crystal を試してみるのが良いでしょう。 Amethyst 27 という Rails ライクな Web フレームワークもあるので、小規模な新規プロジェクト等で試してみるのも良いかもしれません。
FAQ
はてブコメントや Twitter を眺めているときちんと伝わっていないと思われることがあったので簡単に回答します。なお、僕と完全に意見が同じわけではないですが、 @omochimetaru が同じ趣旨でより多様な質問に丁寧に回答しているので、下記で足りなければそちらも御覧下さい。
Q. null 安全がそんなに良いものならとっくに普及しているはずでは?
A. 良いものでもすぐに普及するとは限りません。今では当たり前になっている GC やオブジェクト指向やラムダ式、高階関数が多くの言語に取り入れられるまでにどれだけの時間がかかったか思い出して下さい。
Q. null 安全を導入したらよくわかってない初心者がひどいコードを書いてより悲惨なことになるのでは?
A. この可能性は否定できないですが、この論法では新しいものは何も導入できないことになってしまいます。使いこなすのが著しく難しいものならその指摘も的外れではないですが、 null 安全 なコードを書く上で気を付けなければならないことはそれほど多くなく( nullable のまま値を取り回しすぎない、コンパイラを黙らせるだめに ?:
等でテキトーに値を与えない等)、たとえばオブジェクト指向と比較しても難しくないと思います。
Q. null オブジェクトじゃダメなの?
A. null
オブジェクトには致命的な問題があります。それは null
、つまり値が存在しないときに期待される挙動は文脈によって異なるということです。例えば、こちらのコードの 58 行目に渡すべき適切な null
オブジェクトを実装することはできません。 Integer
が存在しないときに、それが 0
として振る舞って欲しいのか、 1
として振る舞って欲しいのかは文脈によって異なるからです。
Q. 安全性と生産性のトレードオフでは一概に良いものと言えないのでは?
A. null 安全 は 安全性と生産性を同時に高めるもの です。詳しくは本文に追記したのでそちらを御覧下さい。
Q. null でエラーを扱うとエラー情報を扱えず辛くない?
A. エラー情報が必要な場合には null
でエラーを表すべきではないと思います。また、 null
のまま取り回すとどこでエラーが発生したのかわからなくなってしまうので、エラーになる可能性のある関数が nullable を返す場合には、できるだけ早い段階でエラー処理をすべきです。
Q. Option について紹介するならモナドに触れるべきでは?
A. モナドについて詳しい人には改めて説明する必要はないと思いますし、詳しくない人に対してモナドの話まですると混乱するだけかと思い、本投稿では省略しました。
Q. ?. があるから◯◯も null 安全では?
A. ?.
やそれに類似した仕組みを持つ言語は多いですが(そして、 null 安全 な言語の ?.
も Groovy や CoffeeScript, Dart などの動的型付言語由来だと思いますが)、 null 安全 の肝はコンパイル(静的解析)時に null
によるエラーを検出できることなので、 ?.
だけあっても null 安全 にはなりません。
-
null 安全 (または null 安全性 )は null safety の訳語です。 null safety という用語は主に Kotlin コミュニティで使われているもので、言語によって
Optional
やOption
,Maybe
, nullable type などの型で実現できる、null
が原因で実行時エラーを起こさない性質のことを表します。そのような性質を表す言葉として null 安全 はぴったりだと思うので、本投稿では null 安全 という呼び方を採用します。 ↩ -
レガシーコード改善ガイドによるレガシーコードの定義 ↩
-
non-nullable と言うこともありますが( TypeScript など)、本投稿では Kotlin 流に non-null で統一しています。 ↩
-
Kotlin の
if
文は式なのでそのまま戻り値を代入することができますが、他言語のプログラマには馴染みのない使い方なのでここでは文として使用しています。 ↩ ↩2 ↩3 -
Optional.kt - qoncept/kotopt: Kotlin で
sealed class
を使ってポリモーフィズムでSome
とNone
の挙動を切り替えた例 ↩ -
mypy: Experimental strict optional type and None checking ↩ ↩2
-
Suggestion: "safe navigation operator", i.e. x?.y 16 - Microsoft/TypeScript ↩
-
Codcore/amethyst: Crystal 版の Rails ↩