null 安全とは
「イマドキのプログラミングは null 安全がトレンドらしい」(参考記事:null 安全でない言語は、もはやレガシー言語だ)ということで、null 安全初心者が null 安全なコードを書いてみた。
そもそも null 安全とは、「null 参照例外(NullReferenceException)が発生しえるコードをコンパイル時にエラーにしてくれる」仕組み。つまり、コンパイルが通った時点で null 参照例外は起こりえないということになる。革命的に素晴らしいのでは?
C# においては、最新の言語バージョン 8.0 で「null 許容参照型」が導入され、これを使うと null 安全になる。
「null 許容参照型」という名前を聞いて、「参照型なんだから null が許容されるのは当たり前じゃないか」と思ったのだが、その常識が覆る。null 許容参照型を導入することにより、「普通の参照型には null を代入できなくなる」。つまり、C# という言語レベルで過去との互換性が失われる! 革命的に影響が大きい。
あまりにも影響が大きいので C# 8.0 では null 許容参照型はオプション扱いとなり、かなり限定的な環境でないと有効にならない。Visual Studio 16.3 の時点では、プラットフォームを .NET Core にしたうえで(.NET Framework でも一部機能は使えるらしい)、プロジェクトファイルを直接編集して null 許容参照型(Nullable)を有効にして、かつ、関連する警告をエラー扱いにする(参考記事:null 許容参照型)。
コード例
今までの C# のコード
String str = null;
String low = str.ToLower();
があったとして、ここまでシンプルであれば NullReferenceException を予見してコードを修正できるが、str の初期化がどこでされてるか分かりづらい場合は見逃してしまって NullReferenceException が発生するという事態に陥る。
しかし、null 許容参照型を有効にすると「普通の参照型には null を代入できなくなる」ので、str = null の時点でコンパイルエラーとなる。従って、
String str = "HOGE";
String low = str.ToLower();
のようなコードに修正せざるを得なくなり、NullReferenceException が発生しない安全なコードになる。これが null 安全か!
とはいえ、「str が null になることもある」という場合に、null 許容参照型を使う。型名の後ろに「?」を付けるだけだ(null 許容値型と使い勝手は似ている)。
String? str = null;
String? low = str?.ToLower();
str?.ToLower() の部分は、str が null であれば null が返り、null でない場合のみ ToLower() が実行されるので、NullReferenceException が発生しない。str が null の時は low も null になるので、low も null 許容参照型にする必要がある。? を忘れるコンパイルエラーになるので、やはり NullReferenceException は発生しない。null 安全すごい。
いくつかコードを書いてみた感想
null 許容参照型を有効にすると、変数が null になり得るのか否かを常に意識するようになる(というか、不適切なコードはコンパイルエラーになるので意識せざるを得ない)。
特にクラスメンバーを null 許容にしなかった場合は、コンストラクターでの初期化が必要となるので、「null 許容があるべき姿なのかどうか」をしっかり考えて選択するようになった。
関数の書き方も変わってくる。
Boolean TryParse (String s, out Hoge? result) のような関数は、返値が true の場合は result は null にはならず、それをコンパイラに教えるためには [NotNullWhen(true)] のように属性を指定する。が、そんな面倒なことをするくらいなら Hoge? TryParse(String s) のような形式に変更するほうがいいと思う。
また、型キャストの際に as 演算子を使うとキャストできなかった場合に null が返ってきてしまうので、if 文の中で is 演算子を使うスタイルに変わってきた。
private void Func(Object? sender)
{
if (sender is UIElement element)
{
element.AllowDrop = true;
}
}
sender が null ではなく、かつキャストできる場合のみ element に代入されるため、element は null 非許容となり、element のプロパティーにアクセスする際に ? は不要。
一方で、NullReferenceException にならない安全な世界ではあるものの、使い方を間違えるとエラーが埋もれるだけのより深刻な状況にもなりかねない。
呼ばれること自体に意味があるような関数、例えばユーザーへの警告表示で、null 安全ではない世界であれば、
hoge.ShowWarning();
で hoge が null の場合は例外が発生してマズいことになったのが表面化するが、null 安全な世界だと
hoge?.ShowWarning();
のようなコードとなり、例外は発生しないが、ユーザーに警告が表示されず、しかも表示されないままスルーされて問題が埋もれてしまう。
革命は 1 日にして成らず
null 安全は C# にとってあまりにも影響が大きく、ライブラリやコンパイラの対応もまだ発展途上のようだ。
例えば、メンバー変数をコンストラクターで初期化する際、コンストラクターから関数を呼びだしてそこで初期化していても、初期化していないと判定されてしまう。
また、
String[] strs = new String[3];
はコンパイルが通ってしまうが、実際には String[0] などは null であり、本来であればコンパイルエラーにしなければならないコードだ。
環境が整うまでにはまだまだ時間がかかりそうである。
とはいえ、null 安全は非常に素晴らしい仕組みだと思うので、少なくとも今後新規作成するプロジェクトでは積極的に使っていきたいと思う。
気になったこと
マルチスレッドで null 許容参照型にアクセスするとどうなるんだろうか。
想像だが、str?.ToLower() のようなコードはコンパイル時に
if (str != null)
{
str.ToLower();
}
のように解釈されるのではないだろうか。だとすると、str が null ではないと判定した直後に別のスレッドが str を null にしたら、ToLower() 呼出の際に NullReferenceException が発生してしまわないだろうか。
ところで、Nullable の読み方って「ぬらぶる」?
https://docs.microsoft.com/ja-jp/visualstudio/code-quality/using-sal-annotations-to-reduce-c-cpp-code-defects?view=vs-2019