nullはC#で頻繁に登場する特別な値です。
放置するとNullReferenceExceptionの原因になりますが、型と演算子で「nullかもしれない」を明示すれば、多くの不具合を未然に防げます。
本記事では、null許容型(?)とnull合体演算子(??)の基本から注意点まで、順序立てて解説します。
C#のnull安全の基本
nullの意味
nullは「参照が何も指していない状態」を表す値です。
参照型(stringや任意のクラス型など)はnullを取り得ます。
一方で値型(intやdouble、structなど)は原則としてnullを取りませんが、Nullable<T>またはT?を使うとnullを扱えるようになります。
この違いを明確に把握しておくことが、null安全の第一歩です。
nullを参照したままメンバーにアクセスすると、実行時にNullReferenceExceptionが発生します。
次のコードは典型的な例です。
#nullable enable
using System;
class Program
{
static void Main()
{
string? s = null;
// 次の行は実行時にNullReferenceExceptionを発生させます
Console.WriteLine(s.Length);
}
}
実行結果(例):
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
at Program.Main()
nullが起きる場面
現実のコードでは、外部入力や検索の不一致などでnullは自然に発生します。
主な場面を簡単に整理します。
- 外部APIや設定値の取得(存在しないキーの場合)
- コレクション検索の不一致(FirstOrDefaultやFindが見つからない場合)
- 遅延初期化や依存関係の受け渡し(未設定のまま使用)
- 逆シリアライズやDB読み出し(列がNULL)
次の表は代表例と注意点の対応です。
場面 | 例 | 注意点 |
---|---|---|
辞書の検索 | dict.TryGetValue(key, out var v) | 見つからない場合にvが既定値(参照型はnull)になることを前提に処理を分ける |
文字列入力 | 環境変数や引数 | 空文字(“”)とnullは別物です。??はnullのみを補います |
LINQのFirstOrDefault | 配列やリストから検索 | 参照型はnull、値型はdefault(T)が返ることを理解する |
安全に扱う考え方
安全に扱う基本方針は、設計段階から「この変数や戻り値はnullになり得るか」を型で表明することです。
nullの可能性があるならT?、ないならTを使います。
取り出す側では「早めに判定し、早めに代替値を与える」ことが重要です。
条件分岐やnull合体演算子(??)でデフォルト値を明示し、必須項目は受け取り時に例外で止めると、後段での思わぬ例外を防ぎやすくなります。
null許容型(?)の書き方
値型をnull許容にする(int? / Nullable<T>)
値型は通常nullを取りませんが、T?
またはNullable<T>
でnull許容にできます。
int?
とNullable<int>
は完全に等価です。
using System;
class Program
{
static void Main()
{
int? a = null; // intのnull許容
Nullable<int> b = 10; // 等価な書き方
Console.WriteLine(a == null); // True
Console.WriteLine(b.HasValue); // True
}
}
True
True
int?
にすることで、「値がまだ決まっていない/欠損している」状態を自然に表現できるようになります。
参照型のnull注釈(string? と nullableコンテキスト)
C# 8.0以降では、nullable参照型(NRT)機能により、string
は「null非許容」、string?
は「null許容」を表す注釈として扱えます。
プロジェクト全体で有効化するか、ファイル単位で有効化します。
プロジェクト全体(csprojに設定):
<PropertyGroup>
<Nullable>enable</Nullable>
<!-- 必要に応じて <LangVersion>latest</LangVersion> -->
</PropertyGroup>
#nullable enable
有効化すると、コンパイラは静的解析でnullの取り扱いを警告します。
#nullable enable
using System;
class Sample
{
static void Main()
{
string x = "hello"; // null非許容
string? y = null; // null許容
x = null; // 警告: ここにnullを代入する可能性
Console.WriteLine(y.Length); // 警告: nullかもしれない参照の逆参照
}
}
この警告は実行時例外の多くをコンパイル時に気づかせてくれます。
やむを得ず警告を抑制する演算子!
(null許容抑制)もありますが、基本的には原因を取り除く方向で修正することをおすすめします。
取り出し方(HasValue / GetValueOrDefault)
null許容の値型から値を取り出すには、HasValue
やGetValueOrDefault
を使います。
Value
プロパティはnullのときに例外を投げるため、慎重に扱います。
using System;
class Program
{
static void Main()
{
int? score = null;
if (score.HasValue)
{
// score.Valueは安全(HasValueがtrueのときのみ)
Console.WriteLine($"score = {score.Value}");
}
else
{
Console.WriteLine("scoreは未設定(null)です");
}
// nullのときは0(既定値)を返す
Console.WriteLine(score.GetValueOrDefault()); // 0
// 任意の既定値を指定できる
Console.WriteLine(score.GetValueOrDefault(-1)); // -1
// nullなら右辺を使う(??の例は後述)
int safe = score ?? 100;
Console.WriteLine($"安全なスコア = {safe}");
}
}
scoreは未設定(null)です
0
-1
安全なスコア = 100
GetValueOrDefault()
の戻りは、ラップしている型の既定値です(intなら0、boolならfalse、DateTimeなら0001-01-01など)であることに注意してください。
null合体演算子(??)の使い方
基本構文と短絡評価
x ?? y
は、x
がnullでなければx
を、nullならy
を返します。
右辺y
は必要なときだけ評価される(短絡評価)ため、重い処理を右辺に書いても無駄に実行されません。
using System;
class Program
{
static void Main()
{
string? left = "OK";
string RightProvider()
{
Console.WriteLine("右辺を評価しました");
return "Fallback";
}
// leftはnullではないので右辺は評価されない
string result1 = left ?? RightProvider();
Console.WriteLine($"result1 = {result1}");
// leftをnullにして試す
left = null;
string result2 = left ?? RightProvider();
Console.WriteLine($"result2 = {result2}");
}
}
result1 = OK
右辺を評価しました
result2 = Fallback
この短絡評価は副作用や性能面で重要です。
右辺にログ出力や初期化処理を書く場合、左辺が非nullなら一切実行されません。
デフォルト値の設定
??
は入力がnullのときに安全な代替値を与える用途に最適です。
文字列、コレクション、オプション値などに幅広く使えます。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
string? inputName = null;
string name = inputName ?? "(no name)";
Console.WriteLine(name); // (no name)
List<int>? maybeList = null;
// nullなら空のリストを用意する
List<int> list = maybeList ?? new List<int>();
Console.WriteLine($"list.Count = {list.Count}"); // 0
int? pageSizeFromConfig = null;
int pageSize = pageSizeFromConfig ?? 20; // 既定ページサイズ
Console.WriteLine($"pageSize = {pageSize}");
}
}
(no name)
list.Count = 0
pageSize = 20
なお、??
は「nullだけ」を判定します。
空文字""
や0
、false
はnullではないため、右辺は使われません。
空文字も補いたい場合はstring.IsNullOrEmpty
やstring.IsNullOrWhiteSpace
と組み合わせます。
連鎖(a ?? b ?? c)と括弧
??
は連鎖できます。
評価は左から順に行われ、最初に非nullだった値が返ります。
結合規則は右結合なので、a ?? b ?? c
はa ?? (b ?? c)
と同じ意味です。
using System;
class Program
{
static string? A() { Console.WriteLine("A"); return null; }
static string? B() { Console.WriteLine("B"); return "B!"; }
static string? C() { Console.WriteLine("C"); return "C!"; }
static void Main()
{
string result = A() ?? B() ?? C();
Console.WriteLine($"result = {result}");
}
}
A
B
result = B!
上の結果から、A→B→Cの順に必要な範囲だけ呼ばれることがわかります。
Bで非nullが得られたためそれ以上処理を行う必要はなく、Cは呼ばれていません。
他の演算子と混在させると読み間違いが起きやすいので、??
と?:
や||
を併用する場合は必ず括弧で意図を明示するのが安全です。
注意点とベストプラクティス(null許容型・??)
??の左オペランドは参照型/Nullableのみ
??
の左オペランドは「nullを取り得る型」(参照型またはNullable<T>
)でなければなりません。
非nullの値型は使えません。
// コンパイルエラー: 左オペランドは参照型またはNullable<T>である必要があります
int x = 0;
// int y = x ?? 1; // CS0019 などのエラー
また、??
はnullだけを判定します。
空文字はnullではないため、次のコードで右辺は使われません。
using System;
class Program
{
static void Main()
{
string? s = "";
string r = s ?? "default";
Console.WriteLine($"[{r}]"); // [] (空文字のまま)
}
}
[]
空や空白も置き換えたいときは、例えば次のように書きます。
string raw = s ?? "default";
string normalized = string.IsNullOrWhiteSpace(raw) ? "default" : raw;
nullable参照型の有効化(#nullable)
nullable参照型は「警告による見える化」が本質です。
プロジェクト全体で<Nullable>enable</Nullable>
を設定するか、ファイル先頭で#nullable enable
を宣言して使用します。
これにより、null非許容の場所にnullを入れる・nullかもしれないものを逆参照する、といった箇所に警告が出ます。
代表的なパターンを示します。
#nullable enable
using System;
class Demo
{
static void Main()
{
string path = GetPathOrNull(); // 警告: 返り値がstring?なら代入は危険
Console.WriteLine(path.Length); // 警告: pathがnullかもしれない
}
static string? GetPathOrNull() => null;
}
コンパイル時の警告メッセージ例(環境によって異なります):
// warning CS8600: Null リテラルまたは可能性のある null 値を非 null 許容の型に変換しています。
// warning CS8602: null 参照の可能性があるものの逆参照です。
警告は「バグの手がかり」です。
??
や明示的なnullチェックで意図を示し、警告が消える形に修正していくことが推奨です。
??とthrow式で必須チェック
??
はthrow式と組み合わせると「必須入力の即時検証」に便利です。
引数や設定がnullなら、その場で例外を投げて早期に失敗させられます。
using System;
class Program
{
static void Main()
{
string? arg = null;
try
{
string value = Require(arg); // ここで例外が投げられる
Console.WriteLine(value);
}
catch (ArgumentNullException ex)
{
Console.WriteLine($"例外: {ex.GetType().Name}, ParamName={ex.ParamName}");
}
}
// 必須チェック: inputがnullなら例外を投げる
static string Require(string? input) =>
input ?? throw new ArgumentNullException(nameof(input));
}
例外: ArgumentNullException, ParamName=input
コンストラクターや設定読込でも有用です。
public sealed class Service
{
private readonly string _endpoint;
public Service(string? endpoint)
{
_endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint));
}
}
このパターンを使うと、必須依存が満たされないインスタンスが生成されることを防げます。
まとめ
nullは放置すると実行時例外の温床ですが、型と演算子で「nullの可能性」を明示すれば、多くの問題は事前に遮断できます。
値型にはT?
(またはNullable<T>
)、参照型にはstring?
のような注釈を使い、nullable参照型を有効化して警告から学ぶ姿勢が重要です。
値の取り出しはHasValue
やGetValueOrDefault
で安全に行い、デフォルト値の付与には??
を活用します。
??
は短絡評価で右辺の無駄な実行を避け、連鎖で段階的にフォールバックもできます。
なお、左オペランドは参照型/Nullableのみ、空文字はnullではない、といった仕様には注意してください。
さらに、??
とthrow式の組み合わせで必須チェックを早期に行えば、バグの発見が格段に楽になります。
今日から「型でnullを語る」コードに切り替えて、堅牢なプログラムを目指しましょう。