Null安全
Null 安全について記載します。
ゴール
以下について理解していること。
- Null 安全が必要な背景
- スマートキャスト
- 安全呼び出し演算子 (
?
) - 強制呼び出し演算子 (
!!
) - エルビス演算子 (
?:
) - 安全キャスト
null って何よ
null
そのものの意味は、"空" とか "無" とか。ドイツ語らしい。- 変数に
null
を入れると、その変数はどこも参照していない (null
を参照している) 状態になる null
を参照しているオブジェクトのメソッド呼び出しは例外 (NullPointerException
) を発生させるNullPointerException
になるのが期待動作ということは基本的になく、概してプログラミングのミスで発生する
Java のコードで
NullPointerException
が出る例は以下のような場合。- 以下、Java のコード例には「Java」と記載 (特に注釈がなければ Kotlin のコード例)
Java
String s = null; // null を入れる
s.toUpperCase(); // NullPointerException 発生
従来の null との戦い
- NullPointerException と null チェック
- NullPointerException が起きないように、
変数を参照する前に変数がnull
でないかを確認する必要が生じることがしばしばある - 実はきっちり対処しようとするとそれなりに手間
- NullPointerException が起きないように、
Java
// 上述のコード例における NullPointerException を回避する
String s = null;
if (s != null) { // null じゃないことを確認する
s.toUpperCase();
}
- ところが、
- Java の言語仕様上、変数は基本的に全て null になりうる
- その点だけ捉えると全ての変数について使う前に null チェックするのが妥当か…?
- 文脈上、絶対に null でないことが確定しているような場合はチェックするべき?
Java
String s = "hoge";
// s は明らかに null じゃないのでチェックしないで良さそう
String ss = Fuga(); // 文字列か null を返す関数だとしたら
// ss は要チェック
チェックの要・不要を判断するのはプログラマ → 往々にして間違いが起こり得る
アノテーションによる null 回避という案
- Android Java では
@NonNull
のようなアノテーションをメソッドに付与できる @NonNull
をつけておくと、コンパイル時にある程度null
にならないことをチェックできる- しかし基本的に無力
- 表明するためのものでありドキュメンテーションの色合いが強い
- IDE は
@NonNull
なところにnull
になりうる変数を渡すと警告してくれる (こともある) - 制約に違反するコードがあってもコンパイルエラーにはしてくれない
- Android Java では
Java
// アノテーションによって本メソッドは null を返さないことを表明する
@NonNull
String Fuga() {
// ...
return null; // しかし null は返すことはできてしまう (コンパイルエラーではない)
}
// null になりうることも表明できる
@Nullable
String Piyo() {
// ...
}
null 安全
- そんなわけで null にはさんざん苦戦してきた
- Kotlin では、「null になりうるか」「null になりえないか」を「型」で区別している
fun main(args: Array<String>) {
//sampleStart
// たとえば、以下のコードはコンパイルに失敗する
// String は「null になりえない」型であるから
val s: String = null // error: null can not be a value of a non-null type String
//sampleEnd
}
fun main(args: Array<String>) {
//sampleStart
// 「null になりえる」型を宣言するためには、? を補う
// 以下はコンパイル OK
val s: String? = null
// ただし、「null になりえる」の状態のままではメソッド、プロパティの呼び出しができない
// error: only safe (?.) or non-null asserted (!!.) calls are
// allowed on a nullable receiver of type String?
println(s.uppercase())
// たとえ値が入っていても同様
val ss: String? = "hoge"
// error: only safe (?.) or non-null asserted (!!.) calls are
// allowed on a nullable receiver of type String?
println(ss.uppercase())
//sampleEnd
}
- スマートキャスト
- 「null になりえる」の状態では、メソッドやプロパティが呼び出せない
- メソッドやプロパティを呼び出すためには、「null になりえる」の状態から「null になりえない」に変換が必要
- たとえば if 文による null チェックで対象が「null になりえない」ことが分かれば良い
fun main(args: Array<String>) {
//sampleStart
// スマートキャストを使った「null になりえる」から「null になりえない」への変換
val s: String? = "hoge" // 「null になりえる」状態
if (s != null) { // null チェックすることで s は「null になりえない」になる
println(s.uppercase()) // メソッドが呼び出せる
}
//sampleEnd
}
- 安全呼び出し
- 対象が「null だったら null、null じゃなかったら中身」を使うやり方
fun main(args: Array<String>) {
//sampleStart
// ? をつけると「null になりえる」型
val s: String? = "hoge"
// 通常、このままでは s のメソッドを呼べないが、
// ? を補うことで呼び出せる
val u = s?.uppercase()
println(u)
// 上記は以下と同じ処理
val up = if (s != null) {
s.uppercase()
} else {
null
}
println(up)
//sampleEnd
}
- メソッドの引数に「null になりうる」を渡す場合
fun main(args: Array<String>) {
//sampleStart
// 以下の関数の引数の型は「null になりえない」という記述
fun greet(name: String) {
println("hello, $name!")
}
val s: String? = "hoge"
greet(s) // コンパイルエラー
if (s != null) {
greet(s) // これなら OK
}
//sampleEnd
}
- 上記のような例の場合に、
let
を用いることができるlet
は任意の型とラムダ式を引数にとり、ラムダ式を呼び出す
fun greet(name: String) {
println("hello, $name!")
}
fun main(args: Array<String>) {
//sampleStart
val s: String? = "hoge"
// コンパイル OK
// s が null ならラムダ式は呼び出されない
s?.let { greet(it) }
//sampleEnd
}
- 強制呼び出し
!!
を使うと「null になりうる」を「null になりえない」に強制的に変換する- 中身が本当に
null
だったらNullPointerException
になっちゃうので、基本的に使わないで済ませたい - 対象の変数が 100%ヌルでない ということが分かっているなら使っていいかもしれない
- が、そういうときにもあえて強制呼び出しオペレータを用いる必要はなく、もっと良い書き方ができるはず
fun main(args: Array<String>) {
//sampleStart
val s: String? = "hoge"
println(s!!.uppercase()) // コンパイルOK、実行も可能
//sampleEnd
}
fun main(args: Array<String>) {
//sampleStart
val s: String? = null
println(s!!.uppercase()) // コンパイルOK、だが実行時に例外 (NullPointerException) を吐く
//sampleEnd
}
- エルビス演算子
- null のときに返す値を指定できる
- ちなみに三項演算子(
a > 0 ? "1以上" : "0以下"
)とは別物- 三項の方はKotlinでは使えない
fun main(args: Array<String>) {
//sampleStart
val s: String? = null
println(s ?: "null です!") // null だったら「null です!」を出力
// 以下と同義
if (s != null) {
println(s)
} else {
println("null です!")
}
//sampleEnd
}
- 安全キャスト
- 型をキャストする際にも
?
が使える - キャストに失敗したときに例外ではなくて null が返る
- 型をキャストする際にも
fun main(args: Array<String>) {
//sampleStart
val a: Any = "string" // String を Any に入れる
println(a as String) // String へのキャストは OK ("string" が表示される)
println(a as Int) // java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
//sampleEnd
}
fun main(args: Array<String>) {
//sampleStart
val a: Any = "string" // String を Any に入れる
println(a as? String) // String へのキャストは OK ("string" が表示される)
println(a as? Int) // null が表示される
//sampleEnd
}