Edit Page

Nullセーフティ

Nullable型と非nullable型

Kotlinの型システムはnull参照を排除するように作られています。 null参照は10億ドルの過ちとしても知られています。

多くのプログラム言語(Javaも含む)においてもっとも一般的な落とし穴の一つは、 nullリファレンスにアクセスして、null参照の例外になってしまうというものです。 JavaではこれはNullPointerExceptionと言われていて、略してNPEとも呼ばれています。(訳注:日本語だとぬるぽ、と言われているが、少しくだけ過ぎと思うのでこの文書ではNPEと呼ぶ事にする)

KotlinにおけるNPEが起こりうる原因というのは以下しかありません:

  • throw NullPointerException()というコードの明示的な呼び出し
  • 以下で述べる!!の使用
  • 初期化に対するデータの不整合、例えば:
  • Javaとの相互運用:
    • platform typenull参照のメンバにアクセスしようとする場合
    • ジェネリック型をJavaと相互運用する時のnullabilityの問題。例えばKotlinのMutableList<String>にJavaの側でnullを追加してしまうなど。この場合はMutableList<String?>とする必要がある。
    • 外部のJavaのコードによって引き起こされるその他の問題

Kotliでは型システムがnullを保持出来る参照(nullableな参照)と、保持出来ない参照(非nullable参照)を区別します。 例えば、通常のString型の変数はnullを保持出来ません:

fun main() { //sampleStart var a: String = "abc" // 通常の初期化はデフォルトの非nullableを意味する a = null // コンパイルエラー //sampleEnd }

nullを許容する為には、変数をString?と書いてnullableなStringとして宣言しなくてはいけません:

fun main() { //sampleStart var b: String? = "abc" // nullをセット可能 b = null // おっけー print(b) //sampleEnd }

ここで、もしaのメソッドを呼んだりプロパティにアクセスしても、NPEを起こさない事は保証されています。 だから以下のように安全に書く事が出来ます:

val l = a.length

しかしもしbのプロパティにアクセスしようと思えば、それは安全では無い事がありえる。 だからコンパイラはエラーを報告してくれます:

val l = b.length // エラー: 変数 'b' はnullかもしれない

けれど、bのプロパティにアクセスする必要もありますよね?それには幾つかの方法があります。

条件の所でnullをチェックする

まず最初の手段としては、bnullかどうかを明示的にチェックして、二つの可能性を別々に処理する事です:

val l = if (b != null) b.length else -1

コンパイラはあなたが実行したチェックを追跡して、ifの中のlength呼び出しを許可します。 より複雑な条件もサポートされています:

fun main() { //sampleStart val b: String? = "Kotlin" if (b != null && b.length > 0) { print("長さ ${b.length} のString") } else { print("空文字列") } //sampleEnd }

この手段が使えるのはbがイミュータブル(つまりローカル変数でチェックと使う所の間で変更していないか、valのメンバでバッキングフィールドがあるものでoverride可能で無いもの)の時だけです。 なぜならそうでないと、bがチェックの後にnullに変更される可能性があるからです。

セーフコール

二つ目の選択肢としては、セーフコール演算子 ?.を使ってnullableな変数のプロパティなどにアクセスする、というものです:

fun main() { //sampleStart val a = "Kotlin" val b: String? = null println(b?.length) println(a?.length) // 不要なセーフコール //sampleEnd }

これはbがnullで無ければb.lengthを返し、そうでなければnullを返します。 この式の型はInt?となります。

セーフコールは連鎖して使うのに便利です。例えば、Bobは部署に配属されているかもしれない(し、されてないかもしれない)会社員で、 部署には部署長がいるかもしれない、というような時を考えます。 Bobの部署の部署長の名前を(もし居れば)得ようと思えば、以下のように書けます:

bob?.department?.head?.name

このような連鎖的な呼び出しは、どれかのプロパティがnullだったら全体としてもnullを返します。

何らかの処理を非nullの値にだけ行いたい場合は、letとセーフコール演算子を組み合わせて使う事が出来ます:

fun main() { //sampleStart val listWithNulls: List<String?> = listOf("Kotlin", null) for (item in listWithNulls) { item?.let { println(it) } // nullは無視してKotlinは出力 } //sampleEnd }

セーフコールは代入の左辺に使う事も出来ます。 その場合、セーフコールのレシーバが一つでもnullだったらば、その代入はスキップされて、右辺は全く評価されません:

// `person` か `person.department` が nullだったら関数は呼ばれない:
person?.department?.head = managersPool.getManager()

Nullableレシーバ

拡張関数はnullableレシーバに定義する事が出来ます。 こうすることで、null値の振る舞いを指示することが出来て、呼び出す都度nullかどうかをチェックする必要が無くなります。

例えば、toString()はnullableレシーバに対して定義されています。 これはnull値に対しては”null”というStringを返します(nullの値では無く)。 これはロギングとかのシチュエーションで便利です。

val person: Person? = null
logger.debug(person.toString()) // "null"とログに吐かれる。例外は投げられない

もしtoString()を呼んでnullableなStringが結果として欲しければ、セーフコール演算子 ?.を使えばよろしい:

var timestamp: Instant? = null
val isoTimestamp = timestamp?.toString() // String?オブジェクトを返す、この場合は`null`
if (isoTimestamp == null) {
   // timestampが`null`の場合の処理
}

Elvis演算子

あるnullableな参照、例えばbがあるとして、 「もしbnullで無ければその値を使い、そうで無ければ何らかの非nullの値を使いたい」というような場合、以下のように書く事も出来ますが:

val l: Int = if (b != null) b.length else -1

このように完全なif式を書く変わりに、Elvis演算子?:を使うという方法もあります:

val l = b?.length ?: -1

もし?:の左側の式がnullで無ければ、Elvis演算子はその値をそのまま返します。そうでなければ右側の値を返します。 右側の式は左側の式がnullの時だけ評価される事に注意してください。

throwreturnもKotlinでは式なので、 Elvis演算子の右側に使う事が出来ます。

これは関数の引数をチェックする時などに便利です。

fun foo(node: Node): String? {
    val parent = node.getParent() ?: return null
    val name = node.getName() ?: throw IllegalArgumentException("nameがあるのを想定しています")
    // ...
}

!!演算子

NPE好きには三番目の選択肢があります: nullでないと断言する演算子(not-null assersion operator)である !! です。 これはどのような値でも非nullableに変換して、値がnullだったら例外を投げる、というものです。 b!!と書けば、bnullでなければその値(我々の例ではStringの値)を、もしnullならNPEを投げます。

val l = b!!.length

つまり、もしNPEを望むなら、そう振る舞わせる事は出来ます。 ですがそうしたいなら明示的にそう頼む必要があって、何も無い青空から突然降って湧いたりはしません。

セーフキャスト

通常のキャストは、オブジェクトが指定した型で無ければ、ClassCastExceptionになります。 それ以外の選択肢として、キャストの試みが失敗したらnullを返すセーフキャストというものがあります:

val aInt: Int? = a as? Int

Nullable型のコレクション

Nullable型の要素のコレクションがあった時に、nullでない値だけでフィルターして非nullableのコレクションを得たい時は、 filterNotNullを使う事で行なえます:

val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()

次は何を読むべき?