概要
通常、「値型」はnull 値(無効な値)を取れません。ところが、データベース等、一部のアプリケーションでは、値型の通常の(有効な)値とnull(無効な値)を取るような型が欲しいときがあります。そこで、C# 2.0 では、null 許容型(Nullable 型)という特殊な型が用意されました。
Ver. 8.0
C# 8.0 では、参照型についても? の有無で null の可否を指定する機能が追加されました。この機能を指してnull 許容参照型(nullable reference type)と言ったりします。
この null 許容参照型と区別する意味で、本項で説明している機能(C# 2.0 時代には唯一の null 許容型だった)を指して、null 許容値型(nullable value type)と呼ぶこともあります。
ポイント
値型 T に対して、T? をいう書き方で null 許容型になります。
null 許容型は、元となる値型の値か
nullを保持できる型です。
null 許容型
null 許容型(nullable type)は、値型の型名の後ろに? を付ける事で、元の型の値またはnull の値を取れる型になるというものです。int 型で例に取ると、以下のような書き方が出来ます。
int? x = 123;int? y =null;null 非許容型
(本項の意味、すなわち null 許容値型の場合)null 許容型にできるのは「null 許容型を除く値型」のみです。
要するに、int?? のように、「多重に null 許容」な型は作れないということです。int?? と書くとコンパイル エラーになります。
C#の仕様書上は、この「null 許容型を除く値型」を指して、null 非許容型(non-nullable type)と言ったりもします。日本語の場合は「null 非許容」よりも「非 null」とか書く方がわかりやすいかもしれません。
C# 8.0 以降では「null 非許容値型」や「非 null 値型」というように、値型であることを強調する呼び方もします。
null 許容参照型
C# 7.3 以前では、string? というのは定義できません(参照型には? を付けれない)。
C# 8.0 で、null 許容参照型と呼ばれる新しい機能が入って参照型でも? の有無で null の可否を指定できるようになりました。ただ、「後入り」な機能なので、本項で説明している null 許容値型とは少し挙動が違ったりします。
詳しくは別項で説明予定です。
null 許容型のメンバー
T? という書き方で得られる null 許容型は、コンパイル結果的には、Nullable<T>構造体(System名前空間) と等価になります。例えば、以下の2つの変数x と y は全く同じ型の変数になります。
int? x;Nullable<int> y; ちなみに、リフレクションで型情報を取り出そうとした場合、null許容型はNullable<T>構造体に見えます。
そして、このNullable<T>構造体は、HasValueというbool型のプロパティと、ValueというT型のプロパティを持っています。
| 戻り値の型 | プロパティ名 | 説明 |
|---|---|---|
bool | HasValue | 有効な(null でない)値を持っていればtrue、 値がnull ならばfalse を返します。 |
T | Value | 有効な値を返します。 もし、HasValue がfalse(値がnull)だった場合、 例外InvalidOperationException 投げます。 |
また、int? x = 123; という書き方ができることから容易に想像が付くように、T?型 とT 型の間には暗黙の型変換ができます。T →T? の変換は常に可能で、以下のようなコードの下2行は等価になります。
int? x;x = 123;x =new int?(123);// x = 123; と等価。その逆、T? →T の変換は、HasValue がtrue のときのみ可能で、HasValue がfalse の時にはInvalidOperationException がスローされます。
int? x = 123;int? y =null;int z;z = (int)x;// OK。z = (int)y;// 例外が発生。null 許容型に対する演算
元となる型T が持っている演算子は、そのまま null 許容型T? に対して利用できます。
| 単項演算 | + ++ - -- ! ~ | オペランドも計算結果も共にT型の単項演算子がある場合、T?に対してもその演算子を利用できます。T?型のオペランドが null の場合、計算結果も null になります。 |
| 二項演算 | + - * / % & | ^ | (左右両方の)オペランドも計算結果も共にT型の二項演算子がある場合、T?に対してもその演算子を利用できます。T?型のオペランドのどちらか片方でも null だった場合、計算結果も null になります。 (ただし、bool 型に対する&および|は例外で、 これらに関しては後述します。) |
| シフト演算 | << >> | これらも二項演算と同様で、T型の演算子がある場合、T?に対してもその演算子を利用できます。 ただし、シフト演算ですので、右オペランドは int 型です。T?型の左オペランドが null だった場合、計算結果も null になります。 |
| 等値演算 | == != | T型の等値演算がある場合、T?型の等値判定も可能です。T?型の オペランドが左右とも null の場合、比較結果は等しいと判定されます。 また、有効な(non-null の)値と null は等しくありません。 左右ともに有効な値の場合、T型の比較結果と同じになります。 |
| 関係演算 | < > <= >= | T型の比較演算がある場合、T?型の比較も可能です。T?型のオペランドのどちらか片方でも null だった場合、計算結果は false になります。 左右ともに有効な値の場合、T型の比較結果と同じになります。 |
bool? 型に対する& および| は以下のような結果になります。
| x | y | x & y | x | y |
|---|---|---|---|
| true | true | true | true |
| true | false | false | true |
| true | null | null | true |
| false | true | false | true |
| false | false | false | false |
| false | null | false | null |
| null | true | null | true |
| null | false | false | null |
| null | null | null | null |
null 合体演算子 (??)
null 許容型には、?? 演算という特殊な演算子を使えます。この??演算子はnull合体演算子※と呼ばれ、値がnull かどうかを判別し、null の場合には別の値を割り当てる演算子です。
// x, y は int? 型の変数int? z = x ?? y;// x != null ? x : yint i = z ?? -1;// z != null ? z.Value : -1※ coalesce
null合体演算子は、英語では null coalescing operator と言います。
coalesceという名前はSQLの同様の機能から来ているようです。SQLでも、「もし値がnullだったら、別の有効な値を返す」という機能を持ったCOALESCE関数というものがあります。
coalesceの元の英単語の意味は、合体・融合・癒着というような意味です。null coalescing operatorやCOALESCE関数の意味としては、「癒着」が一番近い気がします。SQLが由来ですので、歯抜け(テーブル中のnullの行 = 値が欠けている状態)をパテで埋めるようなイメージでしょうか。
null 合体代入 (??=)
C# 8.0 では、null合体演算子 (??)も複合代入に使えるようになりました(??=)。
例えば以下のような書き方ができます。
staticvoid M(string s =null){ s??="default string";Console.WriteLine(s);}意味としては、if (s == null) s = ...; と同じになります。キャッシュ用途に便利だったりします。
結果の型
C# では、代入や複合代入自体も式になっています。なので、var z = y += x; みたいな感じでつないで掛けて、var z = (y += x); という意味で評価されます。この時、ほとんどの場合、y += x の部分の結果の型はy の型になります。
bytex = 1;bytey = 2;varz = (y +=x);// こう書くと y が byte なので z も byte に。varw =y +x;// この場合は int だったりする。C# の int 未満の整数の足し算結果は int になる。この点に関して、null 合体代入は例外的な挙動をします。というのも、?? の最大の目的は「null だった時に何か有効な値に差し替える」というものなので、結果の型は非 null であってほしい場合がほとんどです。なので、y ??= x の結果の型はy の側ではなく、x の側から推論されます。
#nullableenablestring?s1 =null;strings2 =s1 ??="";// s1 に ? が付いていても、s1 ??= "" の結果は string。int?i1 =null;inti2 =i1 ??= 0;// i1 に ? が付いていても、i1 ??= 0 の結果は int。float?f1 =null;float?f2 =null;float?f3 =f2 ??=f1;// 右辺も null 許容なら結果の方も null 許容。キャッシュ用途で以下のような書き方をよくするため、こういう型決定ルールになっていないと使いにくくなります。
publicT Property => _cache ??=GetValue();privateT? _cache;privateTGetValue(){// 計算に時間がかかる処理}