KotlinDive

プログラミング言語 Kotlin についての入門ブログです。

null安全

null安全について

JavaC++Objective-Cなどの言語では、プログラムを実行するまでオブジェクトがnullであるかどうかがわからない場合があります。 そのため、プログラマーは手動でオブジェクトがnullでないことをチェックするコードを作成して対策する必要があるのですが、 実際にはその対策が不十分であったことで、数多くの致命的な不具合や脆弱性の原因になっています。

この問題は、ビリオンダラーミステイクと言われたこともあります。 (以下に、クイックソートの発明などでも知られる計算機科学者であるアンソニー・ホーアという人の言葉を引用したいと思います)

それは10億ドルにも相当する私の誤りだ。 null参照を発明したのは1965年のことだった。 当時、私はオブジェクト指向言語 (ALGOL W) における参照のための包括的型システムを設計していた。 目標は、コンパイラでの自動チェックで全ての参照が完全に安全であることを保証することだった。 しかし、私は単にそれが容易だというだけで、無効な参照を含める誘惑に抵抗できなかった。 これは、後に数え切れない過ち、脆弱性、システムクラッシュを引き起こし、過去40年間で10億ドル相当の苦痛と損害を引き起こしたとみられる。

Wikipedia

プログラム実行時に誤ってNullを参照することで発生する様々な不具合を回避するため、 最近のプログラミング言語では(プログラムを実行しなくても)ビルド時にnullでないことを保証する「null安全」という仕組みが取り入れられています。
つまり、nullになる可能性になるオブジェクトを(もしオブジェクトがNullだったらまずいような)操作しようとするとビルドエラーが発生するようになっているといういことです。
これが、「null安全」と呼ばれる仕組みです。

nullableとnon-null

nullableな変数の宣言

null安全なプログラミング言語であるKotlinにおいて、 nullable(nullを代入できる)な型とnon-null(nullを代入できない)な型はまったく異なるものです。 「?」が付いている型がnullableになります。


fun main(args: Array) {
    var nullableString: String?

    // nullableな型の変数には、nullを代入できる
    nullableString = null
    println("nullableString=${nullableString}")

    // もちろん、nullでない値を代入することもできる
    nullableString = "nullableです"
    println("nullableString=${nullableString}")

    var nonnullString: String

    // nonnullな方には、nullを代入できない。ビルドエラーになる
    // nonnullString = null

    // nullでない値しか代入できない
    nonnullString = "nonnullです"
    println("nonnullString=${nonnullString}")
}

nullableString=null
nullableString=nullableです
nonnullString=nonnullです

nullableな変数は、その関数を呼び出したりすることができません。 これは、もしその変数がnullだった場合、NGだがらです。


fun main(args: Array) {
    val nullableString: String? = "nullableです"

    // nullableな変数からは、関数を呼び出すことはできません
    // ビルドエラーになります。
    // println(nullableString.length)
}

 

明示的にnullチェックする

nullableな変数を利用するためのもっとも基本的な方法は、ifを利用してnullチェックをすることです。
コンパイラが自動的にnullチェックされているかどうかを判別してくれます。 nullチェックしていれば関数を呼んだりすることができます。


fun main(args: Array) {
    val nullableString: String? = "nullableです"

    // ifで明示的にnullチェックする
    if (nullableString != null) {
        // コンパイラがnullチェック済みかどうかを判定して
        // nullチェック済みならメンバーを利用できる
        println(nullableString.length)
    } else {
        println("nullableString is null.")
    }
}

10

セーフコール

nullableを利用するための2つ目の方法は、セールコールを利用することです。
セーフコールはnullableな変数の後ろに?をつけてメンバを呼ぶことで、 もしその変数がnullでなければ通常の結果が、そうではなくnullならnullが返ってきます。


fun main(args: Array) {
    val nullableString: String? = "nullableです"
    println("nullableString.length = ${nullableString?.length}")

    // nullableなリストの要素にアクセスする
    val fruits = listOf("apple", null, "orange")
    for (fruit in fruits) {
        println(fruit?.length)
    }
}

nullableString.length = 10
5
null
6

エルビス演算子

エルビス演算子はifを利用したnullチェックの表現をシンプルにしたものです。
C++なでにある三項演算子?をnull専用にしたようなものをイメージすればいいと思います。
エルビス演算子は「?:」で表現されます。
単純なセーフコールとの違いは、実際にnullだった場合にどんな値を返すかをプログラマが指定できることです。


fun main(args: Array) {
    val nullableString: String? = "nullableです"

    // もしnullableがnon-nullだった場合はnullableString.lengthが,
    // そうでなくてnullだった場合は-1が返る
    val length = nullableString?.length ?: -1
    println("nullableString.length = ${length}")

    // nullableなリストの要素にアクセスする
    val fruits = listOf("apple", null, "orange")
    for (fruit in fruits) {
        println(fruit?.length ?: -1)
    }
}

nullableString.length = 10
5
-1
6

!!演算子

「!!」(非nullアサーション演算子)はnullableな変数の後ろにつけることで、 もし本当に変数がnullだった場合には、自動的に「null pointer exception」をスローします


fun main(args: Array) {
    val nullableString: String? = "nullableです"

    // もしnullableがnon-nullだった場合はnullableString.lengthが,
    // そうでなくてnullだった場合は-1が返る
    val length = nullableString!!.length
    println("nullableString.length = ${length}")

    // nullableなリストの要素にアクセスする
    val fruits = listOf("apple", null, "orange")
    for (fruit in fruits) {
        println(fruit!!.length)
    }
}

nullableString.length = 10
Exception in thread "main" kotlin.KotlinNullPointerException
	at ***********.main(***********.kt:13)
5

セーフキャスト

「as」演算子を利用してキャストしようとして失敗した場合、 通常はClassCastExceptionがスローされます。
セーフキャストを利用することで、例外をスローせずに安全にキャストすることができます。 セーフキャストは「as?」とすることで利用でき、もしキャストできない場合は、例外をスローする代わりにnullを返します。


fun main(args: Array) {
    val str = "string"

    // キャストに失敗するとnullが返却される
    val num: Int? = str as? Int
    println("num = ${num}")

    // キャストに失敗するとClassCastExceptionがスローされる
    val num2 = str as Int
    println("num2 = ${num2}")
}

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
	at *********.main(HelloWorld.kt:7)
num = null