NullReferenceException は C# 開発で最も頻繁に遭遇するエラーの一つです。「Object reference not set to an instance of an object」というメッセージを見たことがある方は多いでしょう。
本記事では単純な null チェックにとどまらず、スタックトレースの読み方・発生パターン別の原因と対策・null 安全演算子の使いこなし・設計レベルで null を防ぐ手法まで体系的に解説します。値型の Nullable(int?)はNullable型と null 合体演算子を、コンパイラによる null 安全の設計はnull 許容参照型を参照してください。
NullReferenceException とは
NullReferenceException は、null(何も参照していない)の変数に対してメンバー(プロパティ・メソッド・フィールド)にアクセスしようとしたときに発生する実行時例外です。
string text = null; Console.WriteLine(text.Length); // System.NullReferenceException: Object reference not set to an instance of an object.
スタックトレースの読み方
例外発生時に表示されるスタックトレースを読むことで、どのファイルの何行目で null アクセスが起きたかを特定できます。
/*
System.NullReferenceException: Object reference not set to an instance of an object.
at OrderService.CalculateTotal(Order order) ← ここが原因のメソッド
in OrderService.cs:line 42 ← このファイルの42行目
at Program.Main(String[] args)
in Program.cs:line 15
*/
// 上記から OrderService.cs の 42 行目を確認する
// ─── OrderService.cs の 42 行目付近 ────────────
public decimal CalculateTotal(Order order)
{
// order が null、または order.Items が null の可能性がある
return order.Items.Sum(i => i.Price); // ← line 42
}
order was null」のように具体的な変数名が表示される場合があります)を手がかりに原因を特定しましょう。よくある発生パターン 10 選
パターン 1:インスタンスを生成し忘れる
// BAD: フィールドに new を書き忘れる
class ShoppingCart
{
private List<string> _items; // null のまま
public void Add(string item)
{
_items.Add(item); // NullReferenceException!
}
}
// GOOD: フィールド初期化子で初期化
class ShoppingCart2
{
private List<string> _items = new(); // 常に空リストで始まる
public void Add(string item) => _items.Add(item); // 安全
}
パターン 2:戻り値が null のメソッドを確認しない
// BAD: Find / FirstOrDefault などは見つからないと null(または 0/default)を返す
var users = new List<User> { new("Alice", "alice@example.com") };
User found = users.FirstOrDefault(u => u.Name == "Bob"); // 見つからない → null
Console.WriteLine(found.Email); // NullReferenceException!
// GOOD: null チェックをしてから使う
User? found2 = users.FirstOrDefault(u => u.Name == "Bob");
if (found2 is not null)
Console.WriteLine(found2.Email);
// または C# 6+ の null 条件演算子
Console.WriteLine(found2?.Email ?? "ユーザーが見つかりません");
パターン 3:Dictionary から存在しないキーを取得する
var dict = new Dictionary<string, string>
{
["key1"] = "value1"
};
// BAD: [] でアクセスすると KeyNotFoundException(存在しない場合)
// BAD: TryGetValue で取得した変数が null の可能性を見落とす
if (dict.TryGetValue("key2", out string value))
{
// key2 が存在しないのでここには入らない
}
// value は null(string はデフォルト null)
Console.WriteLine(value.ToUpper()); // NullReferenceException!
// GOOD: TryGetValue の成功時のみ使用、または GetValueOrDefault
string? safeValue = dict.GetValueOrDefault("key2"); // null が返る(例外なし)
Console.WriteLine(safeValue?.ToUpper() ?? "キーなし");
パターン 4:LINQ で null 要素を含むコレクションを処理する
var names = new string?[] { "Alice", null, "Charlie", null };
// BAD: null 要素に対してメソッドを呼ぶ
var upper = names.Select(n => n.ToUpper()); // 実行時に null で NullReferenceException
foreach (var u in upper) Console.WriteLine(u); // ← ここで例外
// GOOD: null をフィルタしてから処理
var upper2 = names
.Where(n => n is not null)
.Select(n => n!.ToUpper()); // ! は nullable 抑制演算子(null 除去済みのため安全)
// GOOD: null 条件演算子で変換
var upper3 = names.Select(n => n?.ToUpper() ?? "(null)");
パターン 5:配列の要素が初期化されていない
// BAD: 参照型の配列は各要素が null で初期化される
var users = new User[3];
Console.WriteLine(users[0].Name); // NullReferenceException!
// GOOD: 使う前に各要素を初期化する
var users2 = new User[3];
for (int i = 0; i < users2.Length; i++)
users2[i] = new User($"User{i}", "");
// GOOD: コレクション初期化子を使う
var users3 = new[]
{
new User("Alice", "alice@example.com"),
new User("Bob", "bob@example.com"),
};
パターン 6:連鎖したプロパティアクセスで途中が null
// 深いオブジェクト階層で途中が null になりうる var order = GetOrder(); // Order? を返す // BAD: 途中の Customer または Address が null かもしれない string city = order.Customer.Address.City; // どこかが null なら例外 // GOOD: null 条件演算子 ?. でチェーン全体を安全に辿る string? city2 = order?.Customer?.Address?.City; Console.WriteLine(city2 ?? "住所不明");
パターン 7:非同期処理で null が返る Task
// BAD: async メソッドが null を返してしまう
async Task<string?> FetchDataAsync()
{
try
{
var result = await httpClient.GetStringAsync(url);
return result;
}
catch
{
return null; // エラー時に null を返す
}
}
// 呼び出し側でチェックしないとバグになる
string data = await FetchDataAsync();
Console.WriteLine(data.Length); // NullReferenceException の可能性!
// GOOD: 呼び出し側で null チェック
string? data2 = await FetchDataAsync();
if (data2 is null) throw new InvalidOperationException("データ取得に失敗しました");
Console.WriteLine(data2.Length); // 安全
パターン 8:イベントハンドラーが未登録(null)のまま呼ぶ
class Button
{
public event EventHandler? Clicked;
// BAD: null チェックなしで呼ぶ
public void BadClick()
{
Clicked(this, EventArgs.Empty); // ハンドラー未登録なら NullReferenceException!
}
// GOOD: null 条件演算子で安全に呼ぶ
public void GoodClick()
{
Clicked?.Invoke(this, EventArgs.Empty); // 未登録なら何もしない
}
}
パターン 9:キャスト失敗後に null のまま使う
object obj = "Hello";
// BAD: as キャストは失敗すると null を返す(例外ではない)
Animal? animal = obj as Animal; // null
Console.WriteLine(animal.Name); // NullReferenceException!
// GOOD: is でキャストと null チェックを同時に
if (obj is Animal a)
{
Console.WriteLine(a.Name); // 安全(null にならない)
}
// GOOD: null チェックしてから使う
Animal? animal2 = obj as Animal;
if (animal2 is not null)
Console.WriteLine(animal2.Name);
パターン 10:静的フィールド・遅延初期化の競合
// BAD: 静的フィールドが初期化される前にアクセス
class Config
{
public static Settings? Current; // null のまま
public static void Initialize() => Current = new Settings();
}
// Initialize() を呼ぶ前に Current を使うと null
Config.Current.Load(); // NullReferenceException!
// GOOD: 遅延初期化パターン
class Config2
{
private static Settings? _current;
public static Settings Current => _current ??= new Settings();
}
null 安全演算子の使いこなし
null 条件演算子(?.)と null 条件インデクサー(?[])
string? text = GetText();
// ─── ?. : null なら null を返してチェーンを打ち切る ──
int? length = text?.Length; // null なら null(int?)
string? upper = text?.ToUpper(); // null なら null
bool? isEmpty = text?.StartsWith("A"); // null なら null
// ─── メソッドチェーンにも使える ──────────────────
string? trimmed = text?.Trim()?.ToUpper();
// ─── ?[] : コレクションや配列への null 安全なアクセス
string[]? arr = GetArray(); // null かもしれない配列
string? first = arr?[0]; // arr が null なら null(IndexOutOfRangeException は防げない)
Dictionary<string, User>? cache = GetCache();
User? user = cache?["alice"]; // cache が null なら null
// ─── イベント発火の定石 ────────────────────────
EventHandler? handler = MyEvent;
handler?.Invoke(this, EventArgs.Empty);
null 合体演算子(??)と null 合体代入演算子(??=)
string? name = null;
// ─── ??: 左辺が null なら右辺を返す ──────────────
string display = name ?? "名前なし"; // "名前なし"
int length = name?.Length ?? 0; // 0
// ─── 連鎖できる ──────────────────────────────────
string result = GetPrimary() ?? GetSecondary() ?? "デフォルト";
// ─── ??=: 左辺が null の場合のみ右辺を代入 (.NET Standard 2.1+)
List<string>? items = null;
items ??= new List<string>(); // items が null なら new List を代入
items.Add("apple"); // 安全
// 遅延初期化パターンで頻繁に使う
private string? _cachedValue;
public string CachedValue => _cachedValue ??= ComputeExpensiveValue();
// ─── throw と組み合わせたガード節 ────────────────
string? config = GetConfig();
string safe = config ?? throw new InvalidOperationException("設定が見つかりません");
パターンマッチングによる null チェック(C# 9+)
C# 9 以降では is null / is not null パターンが使え、== null より意図が明確になります。また、== は演算子オーバーロードの影響を受けますが、is null は受けません。
string? name = GetName();
// ─── null チェックの書き方比較 ───────────────────
bool old1 = name == null; // 旧来(演算子オーバーロードに影響される可能性)
bool old2 = name is null; // C# 7+: == と同等だが演算子オーバーロード不影響
bool new1 = name is not null; // C# 9+: 最も明確
// ─── if 文での使い方 ─────────────────────────────
if (name is null)
{
Console.WriteLine("名前が未設定です");
return;
}
// ここから先は name が null でないことをコンパイラが保証
Console.WriteLine(name.Length); // 警告なし
// ─── switch 式との組み合わせ ──────────────────────
string description = name switch
{
null => "未設定",
"" or " " => "空", // パターンマッチングで空白も処理
var n when n.Length > 10 => $"長い名前: {n}",
var n => $"名前: {n}",
};
// ─── 型チェックと同時に null チェック ────────────
object? obj = GetObject();
if (obj is string s && s.Length > 0)
{
Console.WriteLine(s.ToUpper()); // s は string かつ非 null・非空
}
ArgumentNullException.ThrowIfNull(.NET 6+)
メソッドの引数が null の場合に早期エラーを出す「ガード節」を簡潔に書けます。
// ─── 旧来の書き方(.NET 5 以前)─────────────────
public void OldProcess(string name, User user)
{
if (name is null) throw new ArgumentNullException(nameof(name));
if (user is null) throw new ArgumentNullException(nameof(user));
// ... 処理
}
// ─── .NET 6+: ArgumentNullException.ThrowIfNull ─
public void NewProcess(string name, User user)
{
ArgumentNullException.ThrowIfNull(name);
ArgumentNullException.ThrowIfNull(user);
// ... 処理(name, user が null でないことが保証される)
}
// ─── .NET 7+: ArgumentException.ThrowIfNullOrEmpty (文字列専用)
public void StringProcess(string name)
{
ArgumentException.ThrowIfNullOrEmpty(name); // null か空文字で例外
ArgumentException.ThrowIfNullOrWhiteSpace(name); // null・空・空白で例外
// ... 処理
}
// ─── primary constructor での使い方(C# 12)──────
class UserService(IRepository repository)
{
private readonly IRepository _repo
= repository ?? throw new ArgumentNullException(nameof(repository));
}
ArgumentNullException.ThrowIfNull は引数名(nameof)を自動的に取得するため、旧来の throw new ArgumentNullException(nameof(arg)) より簡潔でミスが減ります。DI コンストラクタのガード節に積極的に使いましょう。string 専用の null 安全メソッド
string? s1 = null;
string? s2 = "";
string? s3 = " "; // 空白のみ
string? s4 = "Hello";
// ─── string.IsNullOrEmpty: null か空文字のチェック ─
Console.WriteLine(string.IsNullOrEmpty(s1)); // true
Console.WriteLine(string.IsNullOrEmpty(s2)); // true
Console.WriteLine(string.IsNullOrEmpty(s3)); // false(空白は空文字でない)
Console.WriteLine(string.IsNullOrEmpty(s4)); // false
// ─── string.IsNullOrWhiteSpace: null・空・空白をまとめてチェック
Console.WriteLine(string.IsNullOrWhiteSpace(s1)); // true
Console.WriteLine(string.IsNullOrWhiteSpace(s2)); // true
Console.WriteLine(string.IsNullOrWhiteSpace(s3)); // true ← 空白もはじく
Console.WriteLine(string.IsNullOrWhiteSpace(s4)); // false
// ─── 使い分けの目安 ──────────────────────────────
// バリデーション(ユーザー入力): IsNullOrWhiteSpace(スペースのみ入力も弾く)
// 内部ロジック(空文字を意味ある値として扱う): IsNullOrEmpty
// ─── ?? と組み合わせてデフォルト値を使う ─────────
string display = string.IsNullOrWhiteSpace(s3) ? "(未入力)" : s3;
// ─── null と空文字を統一的に扱う ─────────────────
string? raw = GetUserInput(); // null か空の可能性あり
string normalized = raw?.Trim() is { Length: > 0 } trimmed ? trimmed : "(空欄)";
設計レベルで null を防ぐ
コレクションは null より空コレクションを返す
// BAD: 結果なし → null を返すとすべての呼び出し側で null チェックが必要
public List<Product>? BadSearch(string keyword)
{
if (string.IsNullOrEmpty(keyword)) return null;
return _db.Where(p => p.Name.Contains(keyword)).ToList();
}
// GOOD: 結果なし → 空のコレクションを返す(呼び出し側の null チェック不要)
public List<Product> GoodSearch(string keyword)
{
if (string.IsNullOrEmpty(keyword)) return new List<Product>();
return _db.Where(p => p.Name.Contains(keyword)).ToList();
}
// 使い方の比較
// BAD:
var result = BadSearch("apple");
if (result != null) // 毎回チェックが必要
foreach (var p in result)
Console.WriteLine(p.Name);
// GOOD:
foreach (var p in GoodSearch("apple")) // チェック不要
Console.WriteLine(p.Name);
Null Object パターン:null の代わりに「何もしないオブジェクト」を使う
// インターフェースを定義
interface ILogger
{
void Log(string message);
}
// 実際のロガー
class ConsoleLogger : ILogger
{
public void Log(string message) => Console.WriteLine($"[LOG] {message}");
}
// Null Object: 何もしないロガー(null の代替)
class NullLogger : ILogger
{
public static readonly NullLogger Instance = new();
private NullLogger() { }
public void Log(string message) { } // 何もしない
}
// 利用側
class OrderService
{
private readonly ILogger _logger;
// logger が渡されなければ NullLogger を使う(null チェック不要)
public OrderService(ILogger? logger = null)
{
_logger = logger ?? NullLogger.Instance;
}
public void Process()
{
_logger.Log("処理開始"); // logger が null でも例外が出ない
// ...
}
}
null forgiving 演算子(!)の正しい使い方
// ! 演算子: コンパイラに「ここでは null にならない」と伝える
// 実際に null が入っていると実行時に NullReferenceException が発生するため注意
// ─── 適切な使い方 ─────────────────────────────────
// テストコードや、文脈からnullにならないと確実に分かる場面
string? name = FindUser("admin")?.Name;
// DB に admin は必ず存在すると保証されているロジックで
string definitelyHasName = name!; // コンパイラ警告を抑制
// ─── 不適切な使い方(!を濫用するとnullableの恩恵がなくなる)────
string? value = GetValue();
string forced = value!; // null の可能性を無視して強制 → 爆弾を仕込むのと同じ
Console.WriteLine(forced.Length); // value が null なら実行時に例外
// ─── 推奨: ! より ?? throw を使う ────────────────
string safe = GetValue() ?? throw new InvalidOperationException("値が取得できません");
実践例:null 安全なサービスクラス
record User(int Id, string Name, string? Email, Address? Address);
record Address(string City, string? PostalCode);
class UserService
{
private readonly Dictionary<int, User> _users = new();
// ─── null を返すのではなく例外で知らせる ─────
public User GetUserById(int id)
{
if (id <= 0) throw new ArgumentOutOfRangeException(nameof(id), "ID は 1 以上を指定してください");
return _users.TryGetValue(id, out var user)
? user
: throw new KeyNotFoundException($"ユーザーID={id} が見つかりません");
}
// ─── null を許容する場合は戻り値型を User? にする
public User? FindUserByName(string name)
{
ArgumentException.ThrowIfNullOrEmpty(name);
return _users.Values.FirstOrDefault(u =>
u.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
// ─── ユーザーの住所を安全に取得(多段 null)──
public string GetUserCity(int id)
{
var user = FindUserByName(id.ToString());
// ?. チェーンで安全に辿り、null なら "未設定" を返す
return user?.Address?.City ?? "未設定";
}
// ─── 複数ユーザーを安全に取得(空コレクションを返す)
public IReadOnlyList<User> GetUsersByCity(string city)
{
ArgumentException.ThrowIfNullOrWhiteSpace(city);
return _users.Values
.Where(u => u.Address?.City == city)
.ToList()
.AsReadOnly();
}
// ─── ユーザー情報の表示(null 安全な文字列生成)
public string FormatUser(int id)
{
var user = _users.GetValueOrDefault(id);
if (user is null) return "(ユーザーなし)";
string email = user.Email ?? "メールなし";
string city = user.Address?.City ?? "住所なし";
string postal = user.Address?.PostalCode ?? "-";
return $"{user.Name}({email})/ {city} {postal}";
}
}
よくある落とし穴と注意点
?. でも NullReferenceException は起きる(配列インデックス)
string[]? arr = new string[] { null, "Hello" };
// arr が null → arr?[0] は null(例外なし) ← これは OK
// arr が非 null、arr[0] が null → arr?[0] は null(例外なし) ← これも OK
string? first = arr?[0]; // null(インデックス 0 の要素が null)
// しかし arr?[0] の結果に対してメソッドを呼ぶと?
string? upper = arr?[0]?.ToUpper(); // これは OK(?. を重ねる)
// BAD: arr?[0] が null で .ToUpper() を呼ぶ
// string upper2 = arr?[0].ToUpper(); // arr が非 null かつ arr[0] が null → 例外!
// ↑ ?. は arr に対してのみ適用され、arr[0] の null は保護されない
struct に ?. を使うと int? になる
DateTime? dt = ...; dt?.Year の結果は int? になります。int が必要な場面では dt?.Year ?? 0 のように ?? でデフォルト値を補完してください。
! 演算子を使いすぎると nullable の意味がなくなる
value! の null forgiving 演算子はコンパイラの警告を抑制するだけで、実行時の安全性は変わりません。多用するとせっかくの nullable reference types の恩恵(コンパイラによる null 安全チェック)が失われます。! を書く場面では「本当に null にならない根拠があるか」を一度立ち止まって確認しましょう。
よくある質問
NullReferenceException は「null の参照に対してメンバーアクセスしようとした」実行時エラーです。ArgumentNullException は「メソッドの引数として null が渡された」ことを知らせるために意図的に throw する例外です。前者はバグの症状で後者は防御的プログラミングのツールです。引数チェックには ArgumentNullException.ThrowIfNull()(.NET 6+)を活用しましょう。obj?.Property ?? defaultValue の形で「null なら null を返し(?.)、null ならデフォルト値を使う(??)」と書きます。?.Length ?? 0・?.Name ?? "未設定" のように組み合わせると null 安全かつ簡潔に書けます。IsNullOrWhiteSpace を使いましょう。スペースや全角スペースだけの入力もはじけます。内部ロジックで「空文字は意味のある値」として区別したい場合は IsNullOrEmpty を使います。#nullable enable を有効にしてコンパイラに null 安全を任せる ③ ?.・??・??= 演算子で1行で書く ④ ArgumentNullException.ThrowIfNull でガード節を簡潔にする、の4つを組み合わせると null チェックの量を大幅に減らせます。NullReferenceException のメッセージに「'order' is null」のように具体的な変数名が含まれます(Improved NullReferenceException メッセージ)。それ以前のバージョンでは、疑わしい行を複数の分割した行に書き直してスタックトレースの行番号で絞り込む方法が有効です。まとめ
| 手法・演算子 | 説明 |
|---|---|
?.(null 条件演算子) |
null なら null を返してチェーンを打ち切る |
?[](null 条件インデクサー) |
コレクションへの null 安全なアクセス |
??(null 合体演算子) |
null なら右辺の値を使う |
??=(null 合体代入) |
null なら右辺を代入(.NET Standard 2.1+) |
is null / is not null |
演算子オーバーロードに影響されない null チェック(C# 9+) |
!(null forgiving) |
コンパイラ警告を抑制(濫用禁止) |
ArgumentNullException.ThrowIfNull |
引数 null ガードを簡潔に書く(.NET 6+) |
string.IsNullOrWhiteSpace |
null・空・空白を一括チェック |
| 空コレクションを返す設計 | 呼び出し側の null チェックをなくす |
| Null Object パターン | null の代わりに「何もしないオブジェクト」を使う |
例外処理全般は例外処理完全ガイドを、int? などの値型 Nullable はNullable型と null 合体演算子を、コンパイラによる null 安全設計はnull 許容参照型を参照してください。