2016年3月23日

Kotlin null 許容型 (nullable) の外し方(スマートキャスト)

Kotlin の null 許容型 (nullable type) の変数は、if 等の制御文を挟むことで、null 非許容型 (non-null type) に自動でキャストされる。
null 許容型はコンパイル時にしか存在しないので、バイトコードにキャスト処理が入るわけではない。
IntelliJ IDEA だと、自動キャストされた変数はハイライトされて、マウスオーバーすると「Smart cast to 型名」がポップアップされるので分り易い。

ここでは、どんな制御文で null 許容型からのスマートキャストが可能か(コンパイラがどこまでやれるのか)、色々試してみる。
Kotlin 1.0.1 なので、バージョンが上がると挙動が変わるかも。

1 変数の場合

  1. if で null 除外
    
    fun ifNotNull(x: Any?) {
        if( x != null ) println(x.javaClass)
    }
    
    基本。
  2. if で null の場合、return
    
    fun ifNullReturn(x: Any?) {
        if( x == null ) return
        println(x.javaClass)
    }
    
    (1) の逆バージョン。
  3. when で null 除外
    
    fun whenNotNull(x: Any?) {
        when( x ){
            null -> {}
            else -> println(x.javaClass)
        }
    }
    
    (1) の when バージョン。if 使った方がいい。
  4. when で null の場合、return
    
    fun whenNullReturn(x: Any?) {
        when( x ){
            null -> return
            else -> {}
        }
        println(x.javaClass)
    }
    
    (2) の when バージョン。これも if 使った方がいい。
    また、else を省略するとコンパイルエラー( .javaClass で)。
  5. エルビス演算子 ?: で null の場合、return
    
    fun elvisNullReturn(x: Any?) {
        x ?: return
        println(x.javaClass)
    }
    
    (2) のエルビス演算子バージョン。
    少し魔術っぽいけど、慣れたら問題ない・・・? タイプ量が少なくなるから使いそう。
    return の代わりに例外 throw も可。
  6. ぬるぽ演算子 !! で null の場合、例外
    
    fun nullpo(x: Any?) {
        x!!
        println(x.javaClass)
    }
    
    ほとんどの場合、バッドプラクティス。(ぬるぽ演算子の使い所は難しい・・・)
    !! 演算子は特に名前が無いようなので、"ぬるぽ演算子"の呼び方は非公式
  7. while で null 除外
    
    fun whileNotNull(x: Any?) {
        while( x != null ){
            println(x.javaClass)
            break
        }
    }
    
    (1) の while バージョン。使い所はあまり無い気がするけど、一応可能。
  8. while で null の場合、return
    
    fun whileNullReturn(x: Any?) {
        while( x == null ) return
        println(x.javaClass)
    }
    
    (2) の while バージョン。使い所なんて無い気がするけど、一応可能。
  9. [NG] if-true ネストで if-return
    
    fun ifNullReturnIfNest(x: Any?) {
        if( true ){
            if( x == null ) return
        }
        println(x.javaClass) // コンパイルエラー
    }
    
    ありえないコードは、ネストの中まで見てくれない模様。
  10. while-true ネストで if-return
    
    fun ifNullReturnWhileNest(x: Any?) {
        while( true ){
            if( x == null ) return
            break
        }
        println(x.javaClass)
    }
    
    if と異なり while ならネストの中まで見てくれる・・・。
    while の条件を変数にすると、コンパイルエラー。
    たぶん、while-true がありえるかもしれないコードだからだと推測。
  11. [NG] when-true ネストで if-return
    
    fun ifNullReturnWhenNest(x: Any?) {
        when {
            true -> if( x == null ) return
        }
        println(x.javaClass) // コンパイルエラー
    }
    
    これもありえないコードなので不可。when に else があっても不可。
  12. [NG] with 関数のラムダ式で if-return
    
    fun ifNullReturnWithScope(x: Any?) {
        with( x ){
            if( x == null ) return // この return は ifNullReturnWithScope を抜ける
    
            // 本来、with 内で x にアクセスする際は this を使う
        }
        println(x.javaClass) // コンパイルエラー
    }
    
    VB でお馴染みの With だが、Kotlin では関数 + ラムダ式になっている。(組み込みの構文ではない。)
    with 関数はインライン展開されるけど、さすがに関数の中まで見てくれない。

2 変数の場合

  1. if で null 除外
    
    fun ifNotNull(x: Any?, y: Any?) {
        if( x != null && y != null ){
            println(x.javaClass)
            println(y.javaClass)
        }
    }
    
    単純な複合条件は可能。
    引数無し when でも同様に可能。
  2. if で null の場合、return
    
    fun ifNullReturn(x: Any?, y: Any?) {
        if( x == null || y == null ) return
        println(x.javaClass)
        println(y.javaClass)
    }
    
    (1) の逆バージョン。
    引数無し when (else 有り) でも同様に可能。
  3. if-else-if で順番に null 除外
    
    fun ifNotNullEach(x: Any?, y: Any?) {
        if( x == null ){
            // x は null、y は null かも
        } else if( y == null ){
            // x は 非 null、y は null
            println(x.javaClass)
        } else {
            // x、y ともに非 null
            println(x.javaClass)
            println(y.javaClass)
        }
    }
    
    一度 null を除外すれば、次のブロックでも有効。
    引数無し when でも同様に可能。
  4. [NG] if-else-if で順番に複合条件で null 除外
    
    fun ifNotNullComplex(x: Any?, y: Any?) {
        if( x == null && y == null ){
            // x、y ともに null
        } else if( x != null && y == null ){
            // x は 非 null、y は null
            println(x.javaClass)
        } else if( x == null && y != null ){
            // x は null、y は 非 null
            println(y.javaClass)
        } else {
            // x、y ともに非 null のはず
            println(x.javaClass) // コンパイルエラー
        }
    }
    
    コンパイラが複雑になるから見てないのだと推測。特に変数が増えた場合やばそう・・・
    人間が見ても else ブロックで null 除外されていることは、分かりづらいと思う。

2016年3月10日

Kotlin null 許容型 (nullable) はコンパイル時だけの存在

Kotlin の目玉機能の 1 つ「null 安全」のために null 許容型 (nullable type) が存在するが、これはコンパイル時だけの存在で、実体クラスが定義されているわけではない。
C# / VB.NET 経験者なら Nullable<T> 相当のクラスがあるのか?と思うかもしれないが、そんなものは無い。

たぶん、Java コードとの共存 + 実行時のパフォーマンスが考慮された仕様になっている。

一応、下記で検証可能。

fun main(args: Array<String>) {
    printType<Int>()
    printType<Int?>()
    // Int?::class だとコンパイルエラー (直接 println できない)
}

inline fun <reified T> printType() {
    println(T::class)
}

//[結果]
// class kotlin.Int
// class kotlin.Int
※ 2 行目と 3 行目で生成されるバイトコードは同じ

検証環境

  • jdk 1.8.0_74
  • Kotlin 1.0.0