C# における Nullable(ヌラブル)とは、本来 null を持てない値型に null を許可する仕組み、およびコンパイラが参照型の null 安全性を静的に検査する仕組みの総称です。本記事では、値型の Nullable<T> から始まり、クラスフィールドの扱い、C# 8.0 の Nullable 参照型、そして ??・??=・?. の各演算子までを順に解説します。
Nullable 値型 — int? と Nullable<T>
問題背景:値型は null を持てない
C# の値型(int・double・bool・struct など)は、宣言するだけでゼロ初期化されます。そのため、「値が存在しない」状態を表現する手段がありません。
int age = 0; // 0 なのか、"未設定" なのか区別できない
データベースの NULL を扱う場合や、任意入力フォームの「未入力」を表現したい場合に困ります。
Nullable<T> 構造体
この問題を解決するのが System.Nullable<T> 構造体です。
Nullable<int> age = null;
age = 25;
Console.WriteLine(age.HasValue); // True
Console.WriteLine(age.Value); // 25
age = null;
Console.WriteLine(age.HasValue); // False
// age.Value は InvalidOperationException をスロー
Nullable<T> には 2 つのプロパティがあります。
| プロパティ | 型 | 説明 |
|---|---|---|
HasValue |
bool |
値が格納されているか |
Value |
T |
格納されている値(HasValue が false のときアクセスすると例外) |
int? — シンタックスシュガー
Nullable<int> と書く代わりに、int? という短縮構文が使えます。両者はコンパイラが同一の IL を生成するため完全に等価です。
int? age = null; // Nullable<int> age = null; と同じ
int? score = 95;
Console.WriteLine(age.HasValue); // False
Console.WriteLine(score.HasValue); // True
Console.WriteLine(score.Value); // 95
? 記法はすべての値型に使えます。
double? price = null;
bool? isActive = null;
DateTime? expiry = null;
GetValueOrDefault
HasValue を確認せずに安全に値を取り出したい場合は GetValueOrDefault() を使います。
int? count = null;
int result = count.GetValueOrDefault(); // 0(型のデフォルト値)
int result2 = count.GetValueOrDefault(-1); // -1(指定したデフォルト値)
ボックス化の注意点
Nullable<T> は値型ですが、object へキャストする際の挙動がやや特殊です。null の場合は null としてボックス化され、値がある場合は T としてボックス化されます(Nullable<T> 自体ではなく)。
int? n = 42;
object o = n; // object に 42(int)としてボックス化
Console.WriteLine(o.GetType()); // System.Int32(Nullable<Int32> ではない)
int? m = null;
object o2 = m; // null としてボックス化
Console.WriteLine(o2 is null); // True
クラスのデータフィールドを Nullable にする
クラスのフィールドやプロパティを Nullable 値型として定義する際は、通常の値型フィールドと同じ要領で ? を付けます。
class Employee
{
public string Name { get; set; } = "";
public int? Age { get; set; } // 未登録の場合は null
public double? Salary { get; set; } // 未設定の場合は null
public DateTime? HiredAt { get; set; } // 未入力の場合は null
}
使用例:
var emp = new Employee { Name = "Alice" };
if (emp.Age.HasValue)
Console.WriteLine($"年齢: {emp.Age.Value}");
else
Console.WriteLine("年齢: 未登録");
EF Core でのテーブルマッピング
Entity Framework Core では、Nullable フィールドはデータベースの NULL 許可カラムに自動的にマッピングされます。
class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public double? DiscountRate { get; set; } // NULL 許可カラム
}
Nullable 参照型 — C# 8.0
C# 8.0 以前は、参照型(string・クラスなど)はすべて暗黙的に null になれました。そのため NullReferenceException は実行時まで発見できませんでした。
C# 8.0 では Nullable 参照型(Nullable Reference Types / NRT) が導入され、コンパイラが静的に null 安全性を検査するようになりました。
有効化方法
プロジェクトファイル(.csproj)で有効にします。
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
特定のファイルだけ有効にする場合はディレクティブを使います。
#nullable enable
Nullable 参照型と Non-Nullable 参照型
有効化すると、参照型は 2 種類に区別されます。
| 型 | 記述 | null 許可 |
|---|---|---|
| Non-Nullable 参照型 | string |
不可(警告が出る) |
| Nullable 参照型 | string? |
可 |
#nullable enable
string name = null; // CS8600 警告:null を非 nullable に代入
string? nick = null; // OK:Nullable 参照型
null チェックを強制する
コンパイラは、null かもしれない参照型を逆参照しようとすると警告を出します。
#nullable enable
string? input = GetInput(); // null の場合がある
// 警告なしにアクセスするには null チェックが必要
if (input != null)
{
Console.WriteLine(input.Length); // OK
}
// または null チェックなしにアクセスすると警告が出る
Console.WriteLine(input.Length); // CS8602 警告
Null 免除演算子 !(Null-Forgiving Operator)
コンパイラに「この値は null ではない」と明示的に伝える場合は !(Null-Forgiving Operator)を使います。
string? value = GetMaybeNull();
int len = value!.Length; // 警告を抑制(null なら実行時例外)
使いすぎると NRT の恩恵を失うため、本当に null でないと確信できる場合のみ使用します。
メンバーへの適用
クラスの参照型プロパティにも適用されます。
#nullable enable
class User
{
public string Name { get; set; } = ""; // null 不可
public string? NickName { get; set; } // null 可
public Address? Address { get; set; } // null 可
}
注意点:null 安全はコンパイル時の警告のみ
NRT はランタイムの型システムを変えるものではありません。string と string? は IL レベルでは同じ型です。string と宣言しても実行時に null を代入することは技術的には可能です(ただし警告が出ます)。
Null 関連演算子
Null-Coalescing Operator — ??
?? は左辺が null のとき右辺の値を返す演算子です。「null の場合のデフォルト値」を簡潔に書けます。
string? name = null;
string display = name ?? "名無し";
Console.WriteLine(display); // 名無し
int? score = null;
int result = score ?? 0;
Console.WriteLine(result); // 0
if 文に比べて簡潔に書けます。
// ?? を使わない場合
string display = (name != null) ? name : "名無し";
// ?? を使う場合
string display = name ?? "名無し";
チェーンもできます。
string? a = null;
string? b = null;
string c = "found";
string result = a ?? b ?? c;
Console.WriteLine(result); // "found"
Null-Coalescing Assignment Operator — ??=
??= は左辺が null のときだけ右辺を代入する演算子です。C# 8.0 で導入されました。
string? cache = null;
cache ??= "default value";
Console.WriteLine(cache); // "default value"
// すでに値があれば代入しない
cache ??= "another value";
Console.WriteLine(cache); // "default value"(変わらない)
遅延初期化パターンで特に便利です。
class Config
{
private List<string>? _tags;
public List<string> Tags
{
get
{
_tags ??= new List<string>(); // null のときだけ初期化
return _tags;
}
}
}
Null Conditional Operator — ?. と ?[]
?.(Null 条件演算子)は、左辺が null のときメンバーアクセスをスキップして null を返します。null チェックのためのネストを大幅に減らせます。
string? name = null;
int? length = name?.Length; // null(例外は起きない)
Console.WriteLine(length); // (何も表示されない)
name = "Alice";
length = name?.Length;
Console.WriteLine(length); // 5
?. を使わない場合:
int? length = (name != null) ? name.Length : (int?)null;
メソッド呼び出しにも使えます。
List<int>? numbers = null;
numbers?.Add(1); // null なので何も起きない
int? count = numbers?.Count; // null
チェーン
?. は連続してチェーンできます。途中で null が現れた時点で評価を止め、式全体が null になります。
class Order
{
public Customer? Customer { get; set; }
}
class Customer
{
public Address? Address { get; set; }
}
class Address
{
public string? City { get; set; }
}
Order? order = GetOrder();
string? city = order?.Customer?.Address?.City;
// order が null → city は null
// Customer が null → city は null
// Address が null → city は null
// 全員 non-null → City の値
インデクサーとの組み合わせ ?[]
配列やリストへのインデクサーアクセスにも ?[] が使えます。
int[]? arr = null;
int? first = arr?[0]; // null(IndexOutOfRangeException は起きない)
?. と ?? の組み合わせ
?. と ?? を組み合わせると「null の場合のデフォルト値」を安全に取り出せます。
string? city = order?.Customer?.Address?.City ?? "不明";
Console.WriteLine(city); // null チェーンのどこかが null なら "不明"
まとめ
| 機能 | 記述 | 導入 | 要点 |
|---|---|---|---|
| Nullable 値型 | int? / Nullable<int> |
C# 2.0 | 値型に null を許容する |
HasValue / Value |
.HasValue・.Value |
C# 2.0 | 値の有無を確認して取り出す |
GetValueOrDefault |
.GetValueOrDefault(x) |
C# 2.0 | null 時のデフォルト値を指定して取り出す |
| Nullable 参照型 | string? + <Nullable>enable</Nullable> |
C# 8.0 | 参照型の null 安全性をコンパイラが静的検査 |
| Null-Coalescing | a ?? b |
C# 2.0 | a が null なら b |
| Null-Coalescing Assignment | a ??= b |
C# 8.0 | a が null のときだけ b を代入 |
| Null Conditional | a?.B・a?[i] |
C# 6.0 | a が null ならアクセスをスキップして null を返す |
| Null-Forgiving | a! |
C# 8.0 | コンパイラの null 警告を抑制する |
Nullable 値型と Nullable 参照型は名前が似ていますが仕組みが異なります。前者はランタイムで機能する構造体、後者はコンパイル時の静的解析です。両者を組み合わせることで、コードの意図を明示しながら null 由来のバグを早期に発見できます。