【C#】NullReferenceException完全ガイド|原因パターン・診断・null安全演算子・設計防止策まで

NullReferenceException は C# 開発で最も頻繁に遭遇するエラーの一つです。「Object reference not set to an instance of an object」というメッセージを見たことがある方は多いでしょう。

本記事では単純な null チェックにとどまらず、スタックトレースの読み方・発生パターン別の原因と対策・null 安全演算子の使いこなし・設計レベルで null を防ぐ手法まで体系的に解説します。値型の Nullable(int?)はNullable型と null 合体演算子を、コンパイラによる null 安全の設計はnull 許容参照型を参照してください。

スポンサーリンク
  1. NullReferenceException とは
    1. スタックトレースの読み方
  2. よくある発生パターン 10 選
    1. パターン 1:インスタンスを生成し忘れる
    2. パターン 2:戻り値が null のメソッドを確認しない
    3. パターン 3:Dictionary から存在しないキーを取得する
    4. パターン 4:LINQ で null 要素を含むコレクションを処理する
    5. パターン 5:配列の要素が初期化されていない
    6. パターン 6:連鎖したプロパティアクセスで途中が null
    7. パターン 7:非同期処理で null が返る Task
    8. パターン 8:イベントハンドラーが未登録(null)のまま呼ぶ
    9. パターン 9:キャスト失敗後に null のまま使う
    10. パターン 10:静的フィールド・遅延初期化の競合
  3. null 安全演算子の使いこなし
    1. null 条件演算子(?.)と null 条件インデクサー(?[])
    2. null 合体演算子(??)と null 合体代入演算子(??=)
  4. パターンマッチングによる null チェック(C# 9+)
  5. ArgumentNullException.ThrowIfNull(.NET 6+)
  6. string 専用の null 安全メソッド
  7. 設計レベルで null を防ぐ
    1. コレクションは null より空コレクションを返す
    2. Null Object パターン:null の代わりに「何もしないオブジェクト」を使う
    3. null forgiving 演算子(!)の正しい使い方
  8. 実践例:null 安全なサービスクラス
  9. よくある落とし穴と注意点
    1. ?. でも NullReferenceException は起きる(配列インデックス)
    2. struct に ?. を使うと int? になる
    3. ! 演算子を使いすぎると nullable の意味がなくなる
  10. よくある質問
  11. まとめ

NullReferenceException とは

NullReferenceException は、null(何も参照していない)の変数に対してメンバー(プロパティ・メソッド・フィールド)にアクセスしようとしたときに発生する実行時例外です。

NullReferenceException の最小例
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
}
Visual Studio では例外発生時にデバッガが自動停止し、null になっている変数をホバーで確認できます。「例外の詳細」ウィンドウに表示される変数名(.NET 6 以降では「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 のメソッドを確認しない

メソッドの 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 から存在しないキーを取得する

Dictionary の null 問題
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 要素を含むコレクションを処理する

null 要素を含むコレクションの LINQ 処理
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 チェーンのバグ
// 深いオブジェクト階層で途中が 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

async メソッドで null を返すバグ
// 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)のまま呼ぶ

イベントの 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 のまま使う

as キャスト後の 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 条件インデクサー(?[])

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 は受けません。

is null / is not 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 の場合に早期エラーを出す「ガード節」を簡潔に書けます。

引数の 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 安全メソッド

IsNullOrEmpty と IsNullOrWhiteSpace
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 より空コレクションを返す

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 の代わりに「何もしないオブジェクト」を使う

Null Object パターン
// インターフェースを定義
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 にならない」と伝える
// 実際に 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 安全なサービスクラス

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 は起きる(配列インデックス)

null 条件演算子の誤解
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 にならない根拠があるか」を一度立ち止まって確認しましょう。

よくある質問

QNullReferenceException と ArgumentNullException の違いは何ですか?
ANullReferenceException は「null の参照に対してメンバーアクセスしようとした」実行時エラーです。ArgumentNullException は「メソッドの引数として null が渡された」ことを知らせるために意図的に throw する例外です。前者はバグの症状で後者は防御的プログラミングのツールです。引数チェックには ArgumentNullException.ThrowIfNull()(.NET 6+)を活用しましょう。
Q?? と ?. はどちらを先に使うべきですか?
Aセットで使うのが定番です。obj?.Property ?? defaultValue の形で「null なら null を返し(?.)、null ならデフォルト値を使う(??)」と書きます。?.Length ?? 0?.Name ?? "未設定" のように組み合わせると null 安全かつ簡潔に書けます。
Qstring.IsNullOrEmpty と string.IsNullOrWhiteSpace の使い分けは?
Aユーザー入力のバリデーションには IsNullOrWhiteSpace を使いましょう。スペースや全角スペースだけの入力もはじけます。内部ロジックで「空文字は意味のある値」として区別したい場合は IsNullOrEmpty を使います。
Q毎回 null チェックを書くのが面倒です。いい方法はありますか?
A① 設計で null を排除する(空コレクションを返す・Null Object パターン) ② #nullable enable を有効にしてコンパイラに null 安全を任せる ③ ?.・??・??= 演算子で1行で書く ④ ArgumentNullException.ThrowIfNull でガード節を簡潔にする、の4つを組み合わせると null チェックの量を大幅に減らせます。
Qデバッガを使わずに null の場所を特定するには?
A.NET 6 以降では 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 許容参照型を参照してください。