C# 8.0 から「C の null 値を許容する参照型」という機能が実装されています。
もともと、C# の参照型は null 値を許容しているので、改めて実装される意味がわかりづらいかもしれないです。
現在、null 参照をすることは避けられるようになっていて、C# でも、C# 8.0 から参照型であってもデフォルトでは null を参照しないようになりました。そのため、Visual Studio 2022 なんかでプログラムを書くと、null を参照する(許容する)クラスなんかで、そこそこ CS8600 の警告と出くわすことになります。
実際は、もともと null が許容されていた言語なので、そこから null を完全に排除することは難しいです。なんで、「型によって null の許容/拒否」がわかるように表現されるようになった、というところを押さえておく必要があります。
null を許容する例
テキストを System.Text.JsonSerializer で変換する場合、戻り値は null を許容する型になっています。なので、以下のようになります:
public static class JsonSerializer { ... public static TValue? Deserialize<TValue>(string json, JsonSerializerOptions? options = null); ... }
// CS8600 の警告が出る SampleData data = JsonSerializer.Deserialize<SampleData>(text, options); // OK SampleData? data = JsonSerializer.Deserialize<SampleData>(text, options);
従来は SampleData はクラスなので参照型。なので SampleData
型は null を扱えるため問題なかったのですが、今は見ただけでこの型は null を許容しているのかどうかわかるように書くことになりました。よって、null 値を許容する場合は値型と同じで ?
を付ける必要があります。
これにメリットがあるのかどうか、わかりづらいと思いましたが、あるメソッドから何らかの参照型(クラス)IEnumerable<T>
を返却する場合、取得に失敗したときに返却するデータは、空の IEnumerable<T>
、または null を返却するのか、XML ドキュメント コメントや仕様書に記載が無い場合、従来だと動作がわからないことがありました。(例外を投げる場合も同様)
正常な動作は書いてあるのだけど、上手くいかない場合の動作がわからず、data.Count() > 0
のようなことをして null 参照エラーになる、または、(data?.Count() ?? 0) > 0
のようなことになります。
それが、C#8.0 からは null を許容する参照型は ?
を付けることになったため、デフォルトは null を許容しない参照型になりました。コードを見れば読み取れることが増えてますね。こんな XML ドキュメント コメントは不要になりそう。
/// <summary> /// とある要素の <see cref="SampleData"/> コレクションを取得します。 /// </summary> /// <returns><see cref="SampleData"/> のコレクション。取得に失敗した場合 <value>null</value> を返却します。</returns> public IEnumerable<SampleData> GetSample() => ...;
C# 8.0 からは、SampleData は null 非許容型なので、取得に失敗したときは空の配列を返却するか、例外を投げるだろうと予測できそうです。
補足
null 値を許容する(null-forgiving)
一時的に null を許容する場合は null-forgiving operator を使って警告を回避することもできます。(他言語は not-null assertion, force unwrap など)
!
演算子 (null-forgiving) を利用すると、CS8600 のチェックから外れるので、NullReferenceException がどこで発生してもおかしくない状態になります。
エラーが発生するのは従来どおり、その値を参照したタイミングなので、コードに問題がある箇所よりも後ろになるはずで、変数が渡っていってしまうと、まったく別のところでエラーが発生するのも従来どおり。
SampleData data = JsonSerializer.Deserialize<SampleData>(text, options)!;
アノテーション属性
属性でもいろいろな準備があって、部分的に null を許容したりするような動きを作れる。
public struct Int32 { ... public static bool TryParse([NotNullWhen(true)] string? s, out int result); ... }
int.TryParse 戻り値の戻り値が true
は、翻って s は not null という制約。
string.IsNullOrEmpty
は [NotNullWhen(false)]
。コンパイラの最適化に役立ちます。
値/参照型で null 参照型の実装は全然違う
?
の実装は、値型は Nullable<T>
でしたが、参照型は元々から null を代入できるので整合性の面で微妙なところがあります。
そのあたりはオーバーロードや typeof()
で悩ましい部分が出ていますが、そうそう入り込んだコードでもなければ遭遇しない自体だと思いますが、実装は異なっている、という点は重要です。
ジェネリクス
T
を非 null 参照型だと明示できるようになっています。
class ResasClient<T> where T: notnull { ... }
まとめ
- null を参照できるコードは損失をもたらしてきた歴史がある
- C# も null 参照問題の対策として「型で null 参照/非参照」がわかるようにした
- 基本的には、null 参照問題を避けたいので null を許容しない
- でも、完全に無くすコストは大きいので、部分的に null を許容する