目次

キーワード

概要

通常、「値型」は 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型のプロパティを持っています。

Nullable<T> 型のメンバー
戻り値の型 プロパティ名 説明
bool HasValue 有効な(null でない)値を持っていれば true、 値が null ならば false を返します。
T Value 有効な値を返します。 もし、HasValuefalse(値が null)だった場合、 例外 InvalidOperationException 投げます。

また、int? x = 123; という書き方ができることから容易に想像が付くように、 T?型 と T 型の間には暗黙の型変換ができます。 TT? の変換は常に可能で、 以下のようなコードの下2行は等価になります。

int? x;
x = 123;
x = new int?(123); // x = 123; と等価。

その逆、 T?T の変換は、HasValuetrue のときのみ可能で、 HasValuefalse の時には InvalidOperationException がスローされます。

int? x = 123;
int? y = null;
int z;

z = (int)x; // OK。
z = (int)y; // 例外が発生。

null 許容型に対する演算

元となる型 T が持っている演算子は、 そのまま null 許容型 T? に対して利用できます。

Nullable<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? 型に対する & および | は以下のような結果になります。

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()
{
    // 計算に時間がかかる処理
}

更新履歴

ブログ