概要
通常、「値型」は 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 : y
int 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合体演算子 (??)も複合代入に使えるようになりました(??=)。
例えば以下のような書き方ができます。
static void 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 の型になります。
byte x = 1;
byte y = 2;
var z = (y += x); // こう書くと y が byte なので z も byte に。
var w = y + x; // この場合は int だったりする。C# の int 未満の整数の足し算結果は int になる。
この点に関して、null 合体代入は例外的な挙動をします。
というのも、?? の最大の目的は「null だった時に何か有効な値に差し替える」というものなので、結果の型は非 null であってほしい場合がほとんどです。
なので、y ??= x の結果の型は y の側ではなく、x の側から推論されます。
#nullable enable
string? s1 = null;
string s2 = s1 ??= ""; // s1 に ? が付いていても、s1 ??= "" の結果は string。
int? i1 = null;
int i2 = i1 ??= 0; // i1 に ? が付いていても、i1 ??= 0 の結果は int。
float? f1 = null;
float? f2 = null;
float? f3 = f2 ??= f1; // 右辺も null 許容なら結果の方も null 許容。
キャッシュ用途で以下のような書き方をよくするため、こういう型決定ルールになっていないと使いにくくなります。
public T Property => _cache ??= GetValue();
private T? _cache;
private T GetValue()
{
// 計算に時間がかかる処理
}
