はじめに
C#開発者なら誰もが一度は経験したことがあるでしょう。あの恐怖の瞬間を。
System.NullReferenceException: Object reference not set to an instance of an object.
コンソールアプリなら即座にクラッシュ、Webアプリならエラーページ、デスクトップアプリなら予期しない終了。
そう、NullReferenceExceptionこそが、多くのC#開発者にとって「初めての恐怖体験」なのです。
最近のC#コードを読んでいると、こんな記号を見かけることはありませんか?
string? name = user?.Name;
if (name is not null)
{
Console.WriteLine($"Hello, {name}!");
}
「?
って何?なんでstring
じゃなくてstring?
なの?」
この疑問こそが、今日のお話の出発点です。
今回は、意外と知られていない Nullable Reference Types の使い方と効果を、実例とともに紹介します。
ある日突然の例外(典型例の紹介)
金曜日の夕方、あなたは自信満々でコードをデプロイしました。テストも通っているし、ローカルでも完璧に動いている。しかし、月曜日の朝一番で緊急のバグ報告が...
public class UserService
{
public string GetUserDisplayName(int userId)
{
var user = GetUser(userId);
var displayName = (user?.FirstName + " " + user?.LastName)?.Trim();
return displayName ?? "Unknown"; // ✅ null対策されたバージョン
}
private User? GetUser(int userId)
{
return _context.Users.FirstOrDefault(u => u.Id == userId);
}
}
一見問題なさそうに見えますが、より明確に「NullReferenceException」が起こる例を見てみましょう。
public string GetUserEmail(int userId)
{
var user = GetUser(userId);
return user.Email.ToUpper(); // 💥 userがnullの場合にNullReferenceException!!
}
「でも、テストでは動いていたのに...」
そうです。テストデータには常にユーザーが存在していたからです。本番環境で削除されたユーザーIDが渡された瞬間、悲劇が起こりました。
悲劇の原因分析
1. 型システムが「nullかもしれない」を表現できない
C# 7以前の参照型は、すべて暗黙的にnull許容です。つまり、戻り値が string
となっていても、実際には null
が返る可能性がありました。
public string GetUserName(int userId)
{
var user = FindUser(userId);
return user?.Name; // nullかもしれないのに、戻り値はstring
}
2. 静的チェックが効かない
コンパイラはnullの可能性を知らないので、危険なコードでも平然と通ってしまいます。
string name = GetUserName(123);
Console.WriteLine(name.Length); // 💥 コンパイルエラーにはならない
3. ドキュメントと実装の乖離
「nullを返す可能性がある」とコメントに書いても、それは単なる“お願い”でしかありません。型システムで保証されない限り、見落としや誤解は避けられません。
C# 8.0以降の光:Nullable Reference Types の登場
2019年、ついに C# に救世主が現れました。それが Nullable Reference Types です。
#nullable enable
の魔力
#nullable enable
public string? GetUserName(int userId)
{
var user = FindUser(userId);
return user?.Name;
}
-
string?
:nullを返す可能性があることを明示 -
string
:nullを返さないという契約
警告で守ってくれる世界
string? name = GetUserName(123);
Console.WriteLine(name.Length);
// ⚠️ Warning CS8602: Dereference of a possibly null reference.
これにより、実行前にエラーの芽を摘むことができます。
実践パターン集
1. 明示的な null チェック
if (name != null)
{
Console.WriteLine(name.Length);
}
2. パターンマッチング
if (name is not null)
{
Console.WriteLine($"Hello, {name}!");
}
3. デフォルト値を返す
return name ?? "Unknown User";
4. 条件演算子でスマートに
Console.WriteLine(name?.Length > 0 ? $"Name: {name}" : "No name available");
5. 早期リターンで安全に
if (name is null)
{
Console.WriteLine("User not found");
return;
}
ProcessValidUser(name); // この時点でnameはnon-null
よくある落とし穴と回避テク
❌ Null抑制演算子(!)の乱用
string name = GetUserName(userId)!; // nullだったら即死
→ 使ってもいいが、「ここは絶対にnullじゃない」と言える場合だけ!
Nullable導入時の注意点
外部ライブラリとの連携
string? externalData = ExternalLib.GetData() as string;
Entity Framework
public string Name { get; set; } = string.Empty;
public string? Email { get; set; }
JSONデシリアライズ
if (response?.Data is not null)
{
ProcessUserData(response.Data);
}
プロジェクトへの導入方法
.editorconfig
[*.cs]
dotnet_style_nullable_annotations = true:suggestion
dotnet_style_nullable_context = true:suggestion
.csproj
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
事故歴カタログ(実話風エピソード)
1. 逆さま早期リターン事件
if (user != null) return; // ❌ 本当は user == null の時にリターンしたかった
Console.WriteLine(user.Name); // 💥 NullReferenceException
2. テストの罠
Assert.AreEqual("Test User", GetUserName(testUser.Id));
// 本番では削除済みIDが渡されてnull、そして Lengthで爆発
3. null安全のつもりが...
var name = user?.Name;
Console.WriteLine(name.ToString()); // 💥 nameがnullなら例外
おわりに
Nullable Reference Types は、“Null”という不確実性に型レベルで立ち向かう武器です。
たった1行の #nullable enable
が、あなたのバグを減らし、安心をもたらします。
次に例外が起きた時、思い出してください。
「その例外、回避できたはずだ」
🔗 参考リンク(日本語)
-
Nullable 参照型 - C# リファレンス | Microsoft Learn
└ C# での null 許容参照型(Nullable Reference Types)の概要と構文解説(日本語)
🔗 参考リンク(英語)
- Nullable reference types - C# reference | Microsoft Learn
-
Nullable reference types tutorial - C# | Microsoft Learns)
└ 実践的な導入方法をステップバイステップで学べるチュートリアル