vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

KotlinとNull安全と - null安全 Advent Calendar 2016

はじめに

この記事はnull安全 Advent Calendar 2016の20日目の記事です。

KotlinをベースにNull安全について思うことをつらつらと。

NullableとNonNull

まずNull安全とは何か、というところからですが、ここではNullを許容する変数(Nullable)と許容しない変数(NonNull)とが明確にわけられている、ということとして話を進めたいと思います。

// Nullable: 問題なし
val nullableVariable: String? = null

// NonNull: コンパイルエラー
val nonnullVariable: String = null

私としては変数がNullになり得る、ということよりも、変数にNullが渡された時点で即エラーとなる、というNonNullの仕様によるインパクトが大きいと感じます。

自分で書くコードについて考えると、NullableとNonNullとが別れていることで、Nullにならないことが明らかなのであればNullチェックをせず、Nullになり得る変数に対してのみNullチェックを行う、ということができるようになります。

こうすることでコード量を安全に減らすことができます。

もう一つ、ライブラリの呼び出しなど、自分で書いていないコードにアクセスする場合。

特にListViewのAdapterのように相手から自分のコードが呼ばれる場合、仕様を理解せずに引数を何でもNonNullにしてしまうと、IllegalStateExceptionなどの例外で苦しむハメになります。

例: http://mslgt.hatenablog.com/entry/2016/07/03/191230#20160703_getview_of_listview

// 第二引数をNonNull(convertView: View)にすると、実行時にNullが渡されてエラーが発生する
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View? {
    return convertView
}

そのためNonNullを使うためには、ただNullを渡さないようにするだけでなく、関数が呼ばれるときにどのような値が渡されるのかといった仕様まで正しく理解することが求められる、ということになります。

これらの理解が合わさると、シンプルに安全なコードが書けるようになるのでは?と考えています。

まぁ私はまだまだ例外起こしまくりですが(・_・;)

なおNullableの方は、状況によっては「if(x == null){ y = x.toString() }」などと書かずに「y = x?.toString()」とシンプルに書ける、というのが良いですね。

特にKotlinでは、エルビス演算子(?:)を使ってNullの場合に値を入れるのではなくreturnを実行できるなど、 シンプルに書くための仕組みがあれこれ用意されているのも良いです。

NonNull変数を追う

ではそんなNonNull変数をDecompileしてみるとどんなコードになるでしょうか。

IntelliJ IDEAの Tools -> Kotlin -> Show Kotlin Bytecode -> Decompile でDecompileしたコードを見ることができます。

まずこのようなclassと関数、変数を用意します。

class NullsafeMain : Application() {
    override fun start(primaryStage: Stage){
        var test: String = "test"
        test = null
    }
}

これをDecompileします。

public final class NullsafeMain extends Application {
   public void start(@NotNull Stage primaryStage) {
      Intrinsics.checkParameterIsNotNull(primaryStage, "primaryStage");
      String test = "test";
      test = (String)null;
   }
}

おや?

start関数の引数である「primaryStage」に対しては「@NotNull」が付与されたり、何やら「Intrinsics.checkParameterIsNotNull」なる関数が呼ばれていますね。

ただし、変数である「test」については特に何も行われていませんね。

何か追いかけ方を激しく間違えている気がしてきましたが、せっかくなので「Intrinsics」というclassを辿ってみます。

~省略~
public class Intrinsics {
    ~省略~
    public static void checkParameterIsNotNull(Object value, String paramName) {
        if (value == null) {
            throwParameterIsNullException(paramName);
        }
    }
    ~省略~
    private static void throwParameterIsNullException(String paramName) {
        StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();

        // #0 Thread.getStackTrace()
        // #1 Intrinsics.throwParameterIsNullException
        // #2 Intrinsics.checkParameterIsNotNull
        // #3 our caller
        StackTraceElement caller = stackTraceElements[3];
        String className = caller.getClassName();
        String methodName = caller.getMethodName();

        IllegalArgumentException exception =
                new IllegalArgumentException("Parameter specified as non-null is null: " +
                                             "method " + className + "." + methodName +
                                             ", parameter " + paramName);
        throw sanitizeStackTrace(exception);
    }
    ~省略~

渡されてきた引数(primaryStage)がNullの場合に、IllegalArgumentExceptionを投げてStackTraceにメッセージを出力する、ということのようです。

NonNullの引数にNullが渡された場合の例外はこの処理が実行されているのだと思います。

おわりに

Null安全の機能が必須であるか、と言われると、正直そうではないと思います。

ただ、コードをできるだけシンプルに、かつ安全に実装することを考えたときに、Null安全が強力なツールになるのではないかと思っています。

今回はNonNullの変数にNullを入れたときの処理について追うことができませんでしたが、
こちらについては別途調べてまとめたいと思います。