.NET Frameworkの参照型では、値が未設定であることを表すためにnull/Nothingを使用することができます。 一方int/Integerなどの値型では、nullを代入することはできません。 そのため、未設定や無効な状態であることを表すために0(あるいは-1や最大値などの値)を代入しておくという手法をとる場合があります。 しかし、0という具体的な値を使用してしまうと、未設定であるために0なのか、あるいは0という有効な値が代入されているのか、これら二つの状態があいまいになるという問題があります。

こういった問題を解消するため、値型に対してもnullを代入できるようにしたものがヌル許容型(Nullable型)です。 ヌル許容型を使用すると、値型でもnullが代入されている状態を作り出すことができ、これにより具体的な値が設定されていない状態や無効な状態などを表現することができます。

ヌル許容型に関連して、以下のような演算子・修飾子が導入されています。 詳細については後述しますが、各記号の使い分けやヌル許容型の要点として以下の表をご覧ください。

ヌル許容型に関する修飾子・演算子記号
名前 記号と使用例 機能・動作
Null許容修飾子 int? n
List<int?> list
Dim n As Integer?
Dim list As List(Of Integer?)
値型のヌル許容型化
型をヌル許容にする、ヌル許容型を宣言する
Null合体演算子 x = n ?? 3 ヌル許容型・参照型の非null化
値を参照して、nullの場合は非null値を設定する
Null条件演算子 (list = nullとして)
len = list?.Length
arr = list?.ToArray()
len = list?.ToArray()?.Length
val = list?[0]
null参照のショートサーキット
インスタンスや戻り値がnullの場合に、後続するメンバ呼び出しの結果をnullにする

§1 ヌル許容型

§1.1 ヌル許容型の宣言

ヌル許容型を宣言する場合は、型名の後ろにクエスチョンマーク?を付けます。 例えばC#のint型ならint?、VBのInteger型ならInteger?のようになります。 ヌル許容型int?では、ヌル許容でないint(ヌル非許容型)に対して代入できる値に加えて、null/Nothingを代入することができるようになります。 ヌル許容型で型名の後ろに付けられる?は、Null許容修飾子と呼ばれます。

ヌル許容型変数の宣言
using System;

class Sample {
  static void Main()
  {
    int  i1 = 3;    // 通常のint型
    int? i2 = 3;    // ヌル許容のint型
    int? i3 = null; // ヌル許容型ではnullを設定できる
  }
}

整数型などの基本型だけでなく、任意の値型をヌル許容型にすることができます。 独自に定義した構造体の場合も同様に、Null許容修飾子?を付けるだけでヌル許容型として宣言することができます。

ヌル許容型変数の宣言
using System;

class Sample {
  // 独自に定義した構造体
  struct S {
  }

  static void Main()
  {
    // ヌル許容のbool型
    bool? b = null;

    // ヌル許容のdouble型
    double? d = null;

    // ヌル許容の構造体型
    S? s = null;
  }
}

構造体をヌル許容にする場合に関して、§.ヌル許容型構造体でのフィールド・プロパティの値の変更も参照してください。

§1.2 ヌル許容型の配列・コレクション

ヌル許容型は配列List<T>などのコレクションでも用いることができます。 int?[]List<int?>といったように、配列・コレクションの型にNull許容修飾子?をつければ、ヌル許容型の配列・コレクションを作成することができます。

配列やコレクションで複数の値を扱う際、要素の一部にの状態(empty)や未設定の状態(undefined, uninitialized)を設定したい場合があります。 こういった場合、値型ではnullを用いることができないため、例えば0-1などに特別な意味を持たせる場合がありました。 しかし、0-1未設定などの意味を持たせても、それを認識していなければ単なる数値であることにかわりなく、意味が無視され他の数と同列に処理されてしまう可能性があります。 ヌル許容型を用いれば未設定の状態を表すためにnullを用いることができるようになり、左記のような問題を避けることができます。

ヌル許容型の配列・Listを使用する、要素にnullを設定する
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    // ヌル許容のint型配列に要素としてnullを格納する
    int?[] arr = new int?[] {0, 2, null, 1};

    arr[0] = null; // 配列の要素にnullを設定する

    // ヌル許容のint型Listに要素としてnullを格納する
    List<int?> list = new List<int?>() {0, 2, null, 1};

    list.Add(null); // Listの要素としてnullを追加する
  }
}

この例ではListの初期化にコレクション初期化子を用いています。 コレクション初期化子を用いたインスタンスの作成についてはジェネリックコレクション(1) List §.コレクション初期化子を参照してください。


VBでヌル許容型の配列を宣言する場合、Null許容修飾子?を付ける位置に注意する必要があります。 Null許容修飾子?と、配列を表す配列修飾子()は常にひと組で記述する必要があります。 変数の側に配列修飾子()を付け、型名の後にNull許容修飾子?を付けたり、またその逆に付けるとコンパイルエラーとなります。

Null許容修飾子と配列修飾子の適切な記述位置
Imports System

Class Sample
  Shared Sub Main()
    ' このようにNull許容修飾子と配列修飾子はセットで付ける必要がある
    Dim arr1 As Integer?()
    Dim arr2?() As Integer

    ' このようにNull許容修飾子と配列修飾子を別々に付けることはできない
    Dim arr3() As Integer?
    ' error BC33102: 変数とその型の両方で、Null 許容修飾子 '?' と配列修飾子 '(' および ')' を指定することはできません
    Dim arr4? As Integer()
    ' error BC33102: 変数とその型の両方で、Null 許容修飾子 '?' と配列修飾子 '(' および ')' を指定することはできません
  End Sub
End Class

§1.3 値の設定状態のテスト

§1.3.1 等号演算子・Is演算子によるテスト

ヌル許容型に有効な値(null/Nothing以外の値)が設定されているかどうかを調べるには、==演算子, !=演算子(VBではIs演算子, IsNot演算子)を使ってnull/Nothingと比較します。

ヌル許容型変数に値が設定されているかテストする
using System;

class Sample {
  static void Main()
  {
    int? i;

    // ヌル許容型にnullを設定する
    i = null;

    // 等号演算子でヌル許容型に値が設定されてるかテストする
    if (i == null)
      Console.WriteLine("iには値が設定されていません");
    else
      Console.WriteLine("iには値 '{0}' が設定されています", i);

    // ヌル許容型に値を設定する
    i = 3;

    // 等号演算子でヌル許容型に値が設定されてるかテストする
    if (i == null)
      Console.WriteLine("iには値が設定されていません");
    else
      Console.WriteLine("iには値 '{0}' が設定されています", i);
  }
}
実行結果
iには値が設定されていません
iには値 '3' が設定されています


§1.3.2 HasValueプロパティによるテスト

null/Nothingとの比較の他に、HasValueプロパティをチェックする方法もあります。 ヌル許容型に値が設定されている場合、HasValueプロパティはtrueになります。

HasValueプロパティを使ったチェック
using System;

class Sample {
  static void Main()
  {
    int? i = 3;

    // iに値が代入されているか調べる
    if (i.HasValue)
      Console.WriteLine("iには値 '{0}' が設定されています", i);
  }
}
実行結果
iには値 '3' が設定されています

参照型の場合とは異なり、ヌル許容型変数にnullが設定されている場合にHasValueプロパティを参照しても、ヌル参照(NullReferenceException)にはなりません。 ヌル許容型にnullが設定されている(値が設定されていない)場合、HasValueプロパティはfalseになります。

nullが代入されているヌル許容型変数のHasValueプロパティを参照する
using System;

class Sample {
  static void Main()
  {
    int? i = null; // nullを設定する

    // nullが代入されていてもHasValueプロパティを参照できる(ヌル参照にはならない)
    if (i.HasValue)
      Console.WriteLine("iには値 '{0}' が設定されています", i);
    else
      Console.WriteLine("iには値が設定されていません");
  }
}
実行結果
iには値が設定されていません

§1.4 値の参照・ヌル非許容型への変換

ヌル許容型から値を取り出してヌル非許容型に代入するには、ヌル非許容型への明示的な型変換を行うか、Valueプロパティを参照します。 明示的な型変換を行う場合、拡大変換となる型変換(intlongbyteintなど)であれば型の異なるヌル非許容型への代入を行うこともできます。

ヌル許容型に代入されている値の取り出し
using System;

class Sample {
  static void Main()
  {
    int? i = 3; // ヌル許容型

    int j = (int)i; // 明示的な型変換によって値を取り出す
    long k = (long)i; // 拡大変換となる型変換で値を取り出す
    int l = i.Value; // Valueプロパティを使って値を取り出す

    Console.WriteLine(j);
    Console.WriteLine(k);
    Console.WriteLine(l);
  }
}
実行結果
3
3
3

Valueプロパティは取得専用のため、このプロパティを使って値を設定することはできません。 ヌル許容型への値を設定は、通常の変数と同様に直接代入して行います。


値が設定されていない状態で、ヌル非許容型へのキャストまたはValueプロパティを参照すると例外InvalidOperationExceptionがスローされます。 (NullReferenceExceptionではないので注意)

設定されていない場合での値の取得
using System;

class Sample {
  static void Main()
  {
    int? i = null;

    // 値が設定されていないのでInvalidOperationExceptionがスローされる
    int j = (int)i;
    int k = i.Value;
  }
}
実行結果
ハンドルされていない例外: System.InvalidOperationException: Null 許容のオブジェクトには値を指定しなければなりません。
   場所 System.ThrowHelper.ThrowInvalidOperationException(ExceptionResource resource)
   場所 Sample.Main()

§1.4.1 Null合体演算子・If演算子による値の非null化

C#では値の取り出しにNull合体演算子??を使用することもできます。 この演算子は、ヌル許容型の持つ値を参照する際、値がnullだった場合に代替として用いる非null値を指定する演算子です。 三項演算子?:と似ていますが、よりシンプルに記述できます。 例えばヌル許容型変数aに対してx = a ?? 16という式を記述した場合、aが値を持っている場合はその値、持っていない場合は16xに代入されます。

VBではこれに相当する演算子は用意されていませんが、二項形式のIf演算子を使用することで同様のことができます。 (詳細:論理演算子 §.If演算子)

Null合体演算子による値の参照
using System;

class Sample {
  static void Main()
  {
    int? a = null;
    int? b = 3;

    int x = a ?? 16; // aはnullなので、xには16が代入される
    int y = b ?? 16; // bは3(値を持っている)なので、yには3が代入される

    Console.WriteLine("x = {0}", x);
    Console.WriteLine("y = {0}", y);

    // 三項演算子を使って記述すると次のようになる
    int xx = a.HasValue ? a.Value : 16;
    int yy = b.HasValue ? b.Value : 16;
  }
}
実行結果
x = 16
y = 3

Null合体演算子を用いることにより、値がnullだった場合に別の値を代入するといった操作を、if文による条件分岐や三項演算子?:を用いずに記述することができます。

これと同様の操作は後述のGetValueOrDefaultメソッドを使うことによっても行うことができます。

§1.4.1.1 参照型でのNull合体演算子の使用

Null合体演算子は参照型に対しても用いることができます。 ヌル許容型に対して用いる場合と同様、第一項がnullだった場合は第二項の値が用いられます。 VBの二項形式のIf演算子も参照型に対して同様に動作します。

参照型に対してNull合体演算子を用いる
using System;

class Sample {
  static void Main()
  {
    string a = null;
    string b = "foo";

    string x = a ?? "(null)"; // aはnullなので、xには"(null)"が代入される
    string y = b ?? "(null)"; // bは"foo"(値を持っている)なので、yには"foo"が代入される

    Console.WriteLine("x = {0}", x);
    Console.WriteLine("y = {0}", y);
  }
}
実行結果
x = (null)
y = foo

§1.4.1.2 Null合体演算子と他の型への変換

Null合体演算子では、第一項・第二項ともに同一の型である必要があります(Option Strict Onの場合はVBのIf演算子も同様)。 例えば、次の例のようにヌル許容型の値を型変換するような場合、nullだった場合の値をにはNull合体演算子単体では処理できません。 この場合はif文や三項演算子を用いるか、あるいはNull条件演算子を用います。

値の型を変換するような場面でNull合体演算子を使用したい
using System;
using System.Linq;

class Sample {
  static void Main()
  {
    // この配列をカンマ区切りで"0, 1, (empty), 2"と出力したい
    int?[] arr = new int?[] {0, 1, null, 2};

    // 前提: nullは空の文字列になるため、"0, 1, , 2"と表示されてしまう
    // (nullの場合は"(empty)"と表示したい)
    Console.WriteLine(string.Join(", ", arr));

    // Null合体演算子では異なる型を指定できないのでコンパイルエラーとなる
    //Console.WriteLine(string.Join(", ", arr.Select(val => val ?? "(empty)")));
    // error CS0019: 演算子 '??' を 'int?' と 'string' 型のオペランドに適用することはできません。

    // nullが設定されたヌル許容型に対してToString()を呼び出すと空の文字列(=nullではない)になるため、
    // Null合体演算子の第二項が使用されない
    Console.WriteLine(string.Join(", ", arr.Select(val => val.ToString() ?? "(empty)")));

    // 'Null条件演算子'と組み合わせて用いると、目的の動作となる
    Console.WriteLine(string.Join(", ", arr.Select(val => val?.ToString() ?? "(empty)")));
  }
}
実行結果
0, 1, , 2
0, 1, , 2
0, 1, (empty), 2

Null条件演算子については§.Null条件演算子で別途解説します。

§1.4.2 GetValueOrDefaultメソッドによる値の非null化

Null合体演算子に似たものとして、GetValueOrDefaultメソッドがあります。 このメソッドは、ヌル許容型に値が設定されている場合はその値を、設定されていない場合は型のデフォルト値(0または0に相当する値)を返します。 0以外の値をデフォルト値としたい場合は、引数でデフォルト値として使用する値を指定することもできます。 戻り値はヌル非許容型となるため、デフォルト値としてnull/Nothingを指定することはできません。

このメソッドは、ヌル許容型に値が設定されていない場合はデフォルト値を使って処理を継続させたい場合などに使用することができます。

GetValueOrDefaultメソッドを使ってnullの場合はデフォルト値を用いて値を取得する
using System;

class Sample {
  static void Main()
  {
    int? a = null;
    int? b = null;
    int? c = 3;

    int x = a.GetValueOrDefault();   // aはnullなので、xにはデフォルト値0が代入される
    int y = b.GetValueOrDefault(-1); // bはnullなので、yにはデフォルト値として-1が代入される
    int z = c.GetValueOrDefault(-1); // cは3(値を持っている)なので、zにはcの値=3が代入される

    Console.WriteLine("x = {0}", x);
    Console.WriteLine("y = {0}", y);
    Console.WriteLine("z = {0}", z);
  }
}
実行結果
x = 0
y = -1
z = 3

型のデフォルト値については型の種類・サイズ・精度・値域 §.型のデフォルト値を参照してください。

デフォルト値をnullとして処理を継続させたいような場合は、Null条件演算子を使うことができます。

§1.5 ヌル許容型での演算

ヌル許容型に対しても、ヌル非許容型での演算と同様に加算などの演算を行うことができます。 ただし、演算子の項のどちらか一方がnullの場合は、演算結果もnullとなります。 これに従い、演算結果も必然的にヌル許容型となります。 そのため、ヌル許容型を項に含む演算結果をヌル非許容型に代入することはできません。

ヌル許容型が値を持つ場合の演算結果
using System;

class Sample {
  static void Main()
  {
    int? a = 3;
    int? x = a + 1; // xには4が代入される

    Console.WriteLine(x);

    // ヌル許容型の演算結果をヌル非許容型へ代入することはできない
    // (項のどちらかがnullであれば演算結果がnullとなるため)
    //int y = a + 1;
    // error CS0266: 型 'int?' を 'int' に暗黙的に変換できません。明示的な変換が存在します。(cast が不足していないかどうかを確認してください)
  }
}
実行結果
4

ヌル許容型が値を持たない場合の演算結果
using System;

class Sample {
  static void Main()
  {
    int? a = null;
    int? x = a + 1; // aはnullのため、xにはnullが代入される

    Console.WriteLine(x.HasValue);
  }
}
実行結果
False

§1.6 ヌル許容型でのToStringメソッドによる文字列化

ヌル許容型ではToStringメソッドを呼び出すことはできますが、引数で書式を指定することはできません。 これは、ヌル許容型を構成するNullable<T>構造体ToStringメソッドには書式を指定するバージョンがないためです。 書式と同様、カルチャなどの書式プロバイダを指定することもできません。

そのため、書式を指定してヌル許容型の値を文字列化したい場合は、String.Formatメソッドを使うか、Valueプロパティを参照してその値に対してToStringメソッドを呼び出す必要があります。

ヌル許容型で書式を指定して文字列化する
using System;

class Sample {
  static void Main()
  {
    int i = 3;
    int? ni = 3;

    // 有効桁数4の自然数(N)として文字列化したい
    Console.WriteLine(i.ToString("N4"));
    Console.WriteLine(ni.ToString("N4")); // ヌル許容型では書式を指定したToStringができない
    // error CS1501: 引数を '1' 個指定できる、メソッド 'ToString' のオーバーロードはありません。

    // この場合、String.Formatメソッドによって文字列化する必要がある
    Console.WriteLine(string.Format("{0:N4}", ni));

    // あるいは、Valueプロパティの値に対してToStringメソッドを呼び出す
    Console.WriteLine(ni.Value.ToString("N4"));
  }
}

Nullable<T>構造体については後述の§.Nullable<T>構造体を参照してください。

書式を指定した文字列化に関しては書式指定子、書式プロバイダについてはカルチャと書式・テキスト処理・暦または書式の定義と実装を参照してください。

§1.7 ヌル許容型構造体でのフィールド・プロパティの値の変更

次の例のように、ヌル許容型の構造体でフィールド・プロパティの値を変更しようとした場合、コンパイルエラーとなります。

ヌル許容型構造体でのフィールドの参照
using System;

struct S {
  public int F;
}

class Sample {
  static void Main()
  {
    S? s = new S();

    if (s.HasValue) {
      // 構造体SのフィールドFに代入を行いたい
      s.Value.F = 3;
      // error CS1612: 変数ではないため、'S?.Value' の戻り値を変更できません。
    }
  }
}

このような場合、一旦ヌル非許容の一時変数を使って変更、再代入を行う必要があります。

ヌル許容型構造体でのフィールドの変更
using System;

struct S {
  public int F;
}

class Sample {
  static void Main()
  {
    S? s = new S();

    if (s.HasValue) {
      // いったん一時変数に代入する
      S temp = s.Value;

      // 代入した一時変数を使ってフィールドの値を変更する
      temp.F = 3;

      // もとのヌル許容型変数に代入しなおす
      s = temp;
    }
  }
}

このようにする必要がある理由については値型と参照型 §.値型のプロパティ・インデクサで詳しく解説しています。

§2 Null条件演算子

ヌル許容型や参照型のメソッドを呼び出す場合など、ヌル参照を避ける目的で次のように事前にif文などによるチェックを行う場面が多くあります。 C# 6.0およびVisual Basic 2015以降ではNull条件演算子が新たに導入されていて、このようなヌルチェックの処理をシンプルに記述することができます。

Null条件演算子によるヌル参照の回避
using System;

class Sample {
  static void Main()
  {
    int[] arr = null;

    // 配列の長さを取得して変数lenに代入する
    // (配列がnullの場合はnullが代入される)
    int? len = arr?.Length;
  }
}
if文によるヌル参照の回避
using System;

class Sample {
  static void Main()
  {
    int[] arr = null;

    // 配列の長さを取得して変数lenに代入する
    // (配列がnullの場合は0を代入する)
    int len;

    if (arr == null)
      len = 0;
    else
      len = arr.Length;
  }
}

このようにNull条件演算子は、プロパティ参照やメソッド呼び出しなど、メンバへのアクセスを行おうとした場合にnullかどうかのチェックを行います。 この時、nullだった場合は呼び出しを行わず、nullを結果として返します。 Null条件演算子を記述した場合でも、呼び出し元(Null条件演算子の左項)がnullでなければ通常のメンバアクセスと同様に扱われます。

またNull条件演算子を使う場合、その結果はメンバへのアクセスによって得られる値(プロパティの値・メソッドの戻り値)か、あるいはnullのどちらかになります。 そのため、必然的にNull条件演算子の左辺は参照型あるいはヌル許容型となります。

Null条件演算子とヌル値・非ヌル値での動作
using System;

class Sample {
  static void Main()
  {
    int[] arr = new int[] {0, 1, 2, 3, 4};

    // 変数にnull以外が代入されている場合
    int? len1 = arr?.Length; // arrはnullではないため、len1にはarr.Lengthの値が代入される

    Console.WriteLine("len1 = {0}", len1);

    // 変数にnullが代入されている場合
    arr = null;

    int? len2 = arr?.Length; // arrはnullのためLengthプロパティは参照されず、len2にはnullが代入される

    Console.WriteLine("len2 = {0}", len2);
  }
}
実行結果
len1 = 5
len2 = 

Null条件演算子は、メソッド・プロパティ・インデクサなどの参照に前置することができます。 また、戻り値を持たないメソッドの呼び出しにも使うことができます。 ただし、インデクサの設定に用いることはできません。

Null条件演算子によるメソッド・プロパティ・インデクサの参照
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    List<int> list = null;

    int? len = list?.Count; // プロパティ参照

    int? index = list?.IndexOf(2); // メソッド呼び出し

    list?.Clear(); // 戻り値のないメソッドの呼び出し

    int? e = list?[0]; // インデクサ参照

    // インデクサの設定ではNull条件演算子を用いることはできない
    //list?[0] = 10;
    // error CS0131: 代入式の左辺には変数、プロパティ、またはインデクサーを指定してください。
  }
}

さらに、メソッドチェイン(戻り値から連続するメソッド呼び出し)においてもNull条件演算子を用いることができます。 この場合、呼び出し元や戻り値がnullとなった時点で結果はnullに確定し、それ以降の呼び出しが行われなくなります(ショートサーキット)。

Null条件演算子を使ってnullを扱えるメソッドチェインを記述する
using System;

class Sample {
  static void Main()
  {
    string str = null;

    // メソッドチェインでNull条件演算子を使う
    int? len = str?.Trim()?.Substring(5)?.Length;
  }
}

演算子によってnullを後続に伝播させることができるとも捉えることができるため、Null条件演算子の導入前はNull伝播演算子(null propagating operator)とも呼ばれていました。


このほか、イベントの発生にもNull条件演算子を用いることができます。 イベントの発生は、Null条件演算子によって記述がシンプルになる好例です。

Null条件演算子を使ってイベントを発生させる
using System;
using System.ComponentModel;

class C {
  public event PropertyChangedEventHandler PropertyChanged;

  protected virtual void RaisePropertyChanged1(string propertyName)
  {
    // イベントPropertyChangedを発生させる (if文によるヌルチェック)
    var ev = this.PropertyChanged;

    if (ev != null)
      ev(this, new PropertyChangedEventArgs(propertyName));
  }

  protected virtual void RaisePropertyChanged2(string propertyName)
  {
    // イベントPropertyChangedを発生させる (Null条件演算子によるnullのショートサーキット)
    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }
}

イベントの発生についてはイベント、INotifyPropertyChangedとPropertyChangedEventHandlerによるプロパティ変更の通知についてはプロパティ §.プロパティ変更の通知 (INotifyPropertyChanged)で解説しています。

§3 Nullable<T>構造体

ヌル許容型の実体はNullable<T>構造体です。 そのため、次のようにNullable構造体を使ってヌル許容型の変数を宣言することもできます。 ヌル許容型のValueHasValueなどのプロパティはNullable構造体によって提供されます。

Nullable<T>構造体とヌル許容型
using System;

class Sample {
  static void Main()
  {
    // 以下の二つはどちらもNullable<int>を宣言している
    int? a = 1;
    Nullable<int> b = 2;

    Console.WriteLine(a.Value);
    Console.WriteLine(b.Value);

    a = null;
    b = null;

    Console.WriteLine(a.HasValue);
    Console.WriteLine(b.HasValue);
  }
}
実行結果
1
2
False
False

言語が直接ヌル許容型をサポートしていない場合でも、ジェネリクスを使用できる言語であればNullable構造体を使用してヌル許容型を作成・使用することができます。


Nullable構造体の型引数Tには値型のみを指定できます。 従って、参照型をベースにしたヌル許容型を作成することはできません。

Nullable<T>構造体とヌル許容型
using System;

class Sample {
  static void Main()
  {
    // 値型をベースにしたヌル許容型
    int? i = null;
    bool? b = null;
    Nullable<double> d = null;

    // 参照型をベースにしたヌル許容型を作成することはできない
    string? s = null;
    // error CS0453: 型 'string' は、ジェネリック型のパラメーター 'T'、またはメソッド 'System.Nullable<T>' として使用するために、Null非許容の値型でなければなりません
    Nullable<object> o = null;
    // error CS0453: 型 'object' は、ジェネリック型のパラメーター 'T'、またはメソッド 'System.Nullable<T>' として使用するために、Null非許容の値型でなければなりません
  }
}

また、Nullable構造体を使ってヌル許容型のヌル許容型を構成するといった入れ子にすることもできません。

§3.1 ヌル許容型と型情報

ヌル許容型でGetTypeメソッドを使用して型情報を取得しようとすると、ヌル許容型の元になった型の型情報が返されます。 例えばint?ではSystem.Nullable<System.Int32>ではなくSystem.Int32が返されます。

ヌル許容型に対するGetType
using System;

class Sample {
  static void Main()
  {
    // ヌル許容型
    int? x = 0;

    Console.WriteLine(x.GetType()); // ベースとなる型(int)の型情報が返される
  }
}
実行結果
System.Int32

逆に、typeof演算子・GetType演算子によって型情報を取得する場合、System.Int32ではなくSystem.Nullable<System.Int32>が返されます。

ヌル許容型の型情報の取得
using System;

class Sample {
  static void Main()
  {
    // typeof演算子によってint?の型情報を取得する
    Console.WriteLine(typeof(int?));
  }
}
実行結果
System.Nullable`1[System.Int32]

またC#では、ヌル許容型をis演算子で比較する場合、元の型が同じであればヌル許容型でもヌル非許容型でも同一の型とみなされます。

ヌル許容型とis演算子での比較
using System;

class Sample {
  static void Main()
  {
    // ヌル許容型
    int? x = 0;

    // 以下のどちらもtrueとなる
    if (x is int?)
      Console.WriteLine("x is int?");

    if (x is int)
      Console.WriteLine("x is int");
  }
}
実行結果
x is int?
x is int

このように、is演算子では方がヌル許容型かヌル非許容型かを区別することができません。 ヌル許容型かどうかを調べる方法については後述の§.型あるいは値がヌル許容型かどうかを調べるで解説します。

§3.2 型あるいは値がヌル許容型かどうかを調べる

C#・VBでは、型がヌル許容型かどうかを調べる手段は言語の機能としては用意されていません。 代わりに、Nullable.GetUnderlyingTypeメソッドを使って調べることができます。

Nullable.GetUnderlyingTypeメソッドは、ヌル許容型のベースとなる型の型情報(System.Type)を返します(例えばint?ならint)。 ヌル許容型でない場合、このメソッドはnullを返すので、これによって型がヌル許容型かそうでないかを判別することができます。

Nullable.GetUnderlyingTypeメソッドを使ってベースとなる型の型情報を取得する
using System;

class Sample {
  static void Main()
  {
    Console.WriteLine("int? : {0}", Nullable.GetUnderlyingType(typeof(int?)));
    Console.WriteLine("int : {0}", Nullable.GetUnderlyingType(typeof(int)));
    Console.WriteLine("object : {0}", Nullable.GetUnderlyingType(typeof(object)));
    Console.WriteLine("string : {0}", Nullable.GetUnderlyingType(typeof(string)));
  }
}
実行結果
int? : System.Int32
int : 
object : 
string : 

§.ヌル許容型と型情報でも述べているとおり、ヌル許容型でのGetType()メソッドによる型情報の取得やis演算子による比較では、ベースとなっている型情報に対して行われるため、これを用いてヌル許容型かどうかを調べることができません。 値(インスタンス)がヌル許容型かどうかを調べるには、いくつか方法が考えられます。

ひとつは、以下のようなジェネリックメソッドを用意し、型引数Tに対してNullable.GetUnderlyingTypeを呼び出すことでヌル許容型かどうかを判別する方法です。 ただし、この方法ではObject型に代入されたヌル許容型の値は、ヌル非許容型として判別されます。

インスタンスがヌル許容型かどうかを判別する
using System;

class Sample {
  // 引数で与えられた値がヌル許容型かどうかを調べる
  static bool IsNullable<T>(T val)
  {
    // 型引数Tからベースとなる型を取得できるかどうかでヌル許容型かどうかを判別する
    return Nullable.GetUnderlyingType(typeof(T)) != null;
  }

  static void Main()
  {
    int? x = 0;
    int y = 0;

    Console.WriteLine("x: {0}", IsNullable(x));
    Console.WriteLine("y: {0}", IsNullable(y));

    // この方法ではobject型に代入したヌル許容型を正しく判別できない
    object o = x;

    Console.WriteLine("o: {0}", IsNullable(o));
  }
}
実行結果
x: True
y: False
o: False

この他にもc# - How to check if an object is nullable? - Stack Overflowにて様々な手法が掲載されています。

§3.3 Nullable<T>のデフォルト値

default(T)newによって作成されるデフォルト状態のNullable<T>(Nullable<T>の規定値)は、HasValueがfalseであり、nullが設定されている状態と等しくなります。

Nullable<T>のデフォルト値
using System;

class Sample {
  static void Main()
  {
    var x = new Nullable<int>();
    var y = default(Nullable<int>);

    Console.WriteLine(x.HasValue);
    Console.WriteLine(y.HasValue);
  }
}
実行結果
False
False

§4 ヌル非許容の参照型

値型ValTypeにおけるヌル許容型ValType?に対して、参照型RefTypeをヌル非許容にした型RefType!のようなものは現時点では用意されておらず、作成することはできません。 ヌル非許容型を使用したい場合は、structによって値型とするか、契約プログラミングの手法を用いるなど、他の手段によってnullを非許容とする制約を施すほかありません。