C#8.0に導入された「null 安全」について、いまさらながらまとめてみました。今後、C#を一つの主要な言語として扱っていく中で知っておくべきトピックだと思ったのでまとめます。この記事を機に参照型を使用したときのnullに対しての意識を変えていきたいと思います。
この記事の主要なトピックとなるのがC#における「null 安全」なのですが、これはC#8.0より導入されています。これまでは「null許容値型」という型があったくらいでしたので大きな進歩といえるのかもしれません。
null 安全の前提
前提としてC#では値型(intやstructなど)はnull値は取れません。値型の変数でnull値を代入するには、null許容値型(int?など?を付けて宣言)を使う必要があったのです。その一方、参照型(stringやclassなど)はデフォルトでnullが許容されていて、許容・非許容が区別されていなかったのです。
ここで登場したのが「null許容参照型」となります。この機能の登場により、C#でも参照型でもnull許容、null非許容の区別が付くようになった場合、null参照例外が発生するコードはコンパイル時に検出してくれるようになりました。
これは「実行時にnull参照例外がスローされる心配がなくなる」ということですね。コードレベルでは「基本的にnullをとり得るものはエラーとするから、nullの取り扱いを考えて実装してね」ということの言い換えかもしれません。要するに「nullable と non-null を型で区別し、 null による実行時エラーを防止する仕組み」をnull安全と呼んでいる、ということになります。
nullの問題点とは
1965年に考案したnull参照の概念は、10億ドル単位の過ちと呼ぶべきものであろう。これこそがその後40年に渡り、数え切れないほどのエラーや脆弱性、システムクラッシュの原因となり、10億ドル単位の損害や苦労を引き起こしてきたのである。
こんな言葉が飛び出すほど、nullはアプリケーションに対して影響を及ぼしてきました。
var person = TryGetPerson(id);
string name = person.Name;
上記のようなコードがあった場合に、変数・personがnullで返却されてきたとするとpersonのプロパティであるNameにアクセスしようとすると、いわゆる「null参照」で落ちることになります。回避するにはnullを評価する判定文を書いてあげる必要がありました。
var person = TryGetPerson(id);
if( person == null )
return;
string name = person.Name;
上記のように参照型を扱う場合は、様々な場面でnullを想定しておく必要があり、nullチェックができていないところではnull参照で落ちる危険性がありました。nullが持つ最大の問題は初期化に関わる不定動作により、nullが来ることを期待していないのにnullが来てしまうバグが発生してアプリが落ちるというものだったのです。
C#8.0からのnull参照許容型
前提でも記載した通り、これまでのC#は値型(intやstructなど)はnull値は取れませんでした。値型でnull値を代入可能とする場合には、null許容値型を使って宣言する必要がありました。その一方で、参照型(stringやclassなど)はデフォルトでnull許容型であり、null値についての許容・非許容については区別されていませんでした。
そうした状況に対する回答として、C#8.0では「null参照許容型」が導入されています。C#8.0においてnull参照許容型を有効にした状態でコーディングを行うと、参照型のTに対してnullを認めなくなります。nullを許容するには、T? の形で記述しなくてはなりません。これによってビルド時にエラーとなり「null安全」が満たされるようになりました。
// Objectはnullを設定できず警告となる
Object testObj1 = null;
// Object?はnullを設定できるので警告とならない
Object? testObj2 = null;
この機能には「null判定を加味した実装をする必要がなくなる」という利点が生まれます。これまでの実装では想定外なところでnullが入ってくることを考えて判定をする必要があり、多少なりともコードに冗長性があったことは否めません。
var obj = DoSomethingWithMethod();
if( obj == null )
{
// objがnullの場合の処理
....
}
//objがnullでない場合の処理
....
これまでの実装では上記のようなコードが至る所に埋め込まれ、毎回「nullの扱いはどうする?」と頭を悩ませていたプログラマーも多かったのではと思います。そうした「想定外」を取り払って「nullの扱いをどうする」という前提を作ることで実装の手間を軽減できるようになったのが「null参照許容型」ということになります。
null参照許容型の演算子
そうした条件でソースコードを記述する場合は、つねにnullかどうかの判定を行った上で後続処理を実行する必要があるのですが、それを一発で行ってくれる便利な条件演算子です。大きく分けるとnullを評価するための演算子が3つほど用意されています。以下に挙げてみました。
- null 条件演算子
- null 合体演算子
- null 合体代入演算子
null 条件演算子(?. / ?[])
ソースコードで記述される ?. や ?[] はnull条件演算子と呼ばれます。?. / ?[] はメンバーやインデックスのアクセスの前に、左辺がnullかどうかを判定し、nullでない場合にnullでない場合、アクセスが行われます。左辺がnullの場合、nullが返ります。
下記のコードはuserがnull出ない場合、Nameの値が返り、nullの場合にはnullを返します。userがnullでもNullReferenceExceptionは発生しません。
var name = user?.Name;
またコレクションである場合も同様に、メンバーがnullかどうかを判定してからアクセスをするようになります。
var secondName = users?[0];
usersのコレクションがnullでない場合は指定のインデックスにアクセスし、nullである場合は何事もなかったかのようにnullを返却します。うっかりのnull参照を防ぐためにも、null許容型に対してきちっと演算子をつける必要があるということです。
null 合体演算子
?? は「null合体演算子」や「nullカスケード演算子」と呼ばれます。左辺がnullでない場合は左辺を返し、nullである場合は記述さあれた右辺を返す処理を実行します。下記の例では「??」の左辺がnullだった場合に右辺の値を返却するようになります。
var userName = GetUserName(userId) ?? "User";
null 合体代入演算子
左辺の式がnull値である場合のみ値を代入できるようにする「null 合体代入演算子」が登場しています。下記の例ではuserNameがnullだった場合を評価してイコールの右側の値が代入されます。
string userName = null;
userName ??= "DefaultUser";
null演算子の合わせ技
かなりアクロバティックな感じはありますが、上記に挙げたnull演算子を合わせて使うことが可能です。石橋をたたいて渡るような感じかもしれませんね。
var user = GetUserById(userId);
var userName = user?.Name ?? "User";
参考資料
null安全について考えるために読んだ資料・記事のリンクはこちら。