連載Index(読む順・公開済(リンク)はここが最新): S00_門前の誓い_総合Index
落ちた理由が NullReferenceException だった。
ログを見ると「どこでnullが入り込んだか」が分からない。
しかも、例外が表に出た場所は、nullが入り込んだ起点(設定取得/IO受信/画面入力/外部API受領/DI注入など)とは限らないことが多い。
このページは、起点の近くで止めるために「警告(有効化)」「型(string?)」「境目(Guard)」を先に揃える。
1. このページで手に入るもの(最短)
狙い: スクロールの価値を最初に確定させ、どこから読んでも迷子にならない状態へ寄せる。
- null 許容参照型(Nullable Reference Types)を有効化し、「入り込み得る経路」を警告で見える化する
- 「外から入る」直後で止めるGuard(境目)を用意し、null(と空/空白)をそこで止める
- 内部は
string/ 非nullableで揃え、null判定を散らさない - 最短テンプレ(コピペ): 設定取得直後で必須
stringを止めるRequire(コメント込み) -
!(null許諾)を置ける条件 / 置けない条件の線引きと、警告消しを増殖させないルール -
?./??/??=の定義と、nullable連鎖を増やさない使い分け
2. 先に逆引き(症状→原因→対策)
狙い: 症状から最短で“見る場所”へ飛び、切り分けの距離を短くする。
| 症状 | ありがちな原因 | 見る場所 | 最短の対策 | 再発防止(ルール化) |
|---|---|---|---|---|
| 本番だけ落ちる | 設定値が欠ける/空 | 設定の解決順と取得箇所 | 設定取得直後でGuard | 設定取得を共通化 |
| 操作により落ちる | UI値がnull/型違い | UI値取得直後 | UI値取得直後でGuard | UI入力の扱いを統一 |
| たまに落ちる | LINQ結果が空 |
FirstOrDefault周り |
?? throwで扱いを決める |
空の扱いを例外/Tryへ統一 |
| 警告は消えたが落ちる |
!で押し切った |
!の出現箇所 |
!撤去、根拠をコード化 |
!の利用条件を決める |
| “nullかも”が連鎖する | nullableが内部まで残る | 引数/戻り値の注釈 | 境目でnon-nullへ寄せる | 契約(string/string?)を揃える |
3. 最短テンプレ(コピペ)
狙い: 境目(設定取得直後)で止める形を先に置き、後段のnull判定を増やさない。
結論: 必須値は取得直後で止め、以降は string 前提へ寄せる。
// 目的: 外から入る値(設定/入力/IO等)を受けた直後で必須条件を確定させる
// 境目: 設定取得直後 / UI値取得直後 / IO直後 / 生成直後
public static string Require(string? value, string name)
=> !string.IsNullOrWhiteSpace(value)
? value
: throw new InvalidOperationException($"{name} is missing.");
// 使い方: 設定取得直後で止め、以降は string 前提で進める
var apiKey = Require(config["ApiKey"], "ApiKey");
4. null 許容参照型の位置付け(ここを誤解しない)
狙い: string? が何を意味するかを先に揃え、契約(必須/任意)が曖昧にならない状態へ寄せる。
結論: null 許容参照型は実行時にnullを禁止する仕組みではなく、コンパイラが「nullになり得る経路」を警告として出す仕組み。
4-1. string と string? の意味
狙い: 必須/任意を型で表し、止める責務が散らない状態へ寄せる。
-
string: nullを許さない契約 -
string?: nullの可能性を契約として認める
契約が揃うと、どこで止めるか(責務)が散りにくい。
4-2. int? と string? の違い
狙い: 同じ ? に見えて別物である点を先に揃え、誤解を減らす。
-
int?:Nullable<int>(値型のnull許可) -
string?: 参照型のnull可能性を注釈で表す(null 許容参照型)
同じ ? でも意味が違う。
5. 有効化(プロジェクト設定)
狙い: 「入り込み得る経路」を警告として先に見える化し、混入点の近くで止める材料を増やす。
結論: この設定は実行時の挙動を変えるものではない。代わりに、型注釈(string? 等)と警告で「どこからnullが入り得るか」を見える形へ寄せる。
何が良くなるか(現場で効く順):
-
string前提の場所へstring?が混ざる経路が警告で浮く -
!で押し切って警告だけ消す形が見つかりやすい - “どこで止めるか”を設定取得直後/IO直後/UI値取得直後へ寄せやすい
5-1. csproj
狙い: まず enable で警告を出し、混入点を可視化する。
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
段階的に強くする例:
<PropertyGroup>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
補足:
- 既存資産が多い場合、まず
enableで警告を可視化し、増え続けない運用へ寄せるのが現実的
6. Guardの置き場(止める場所が前に寄るほど短くなる)
狙い: 例外が表に出る場所の揺れを減らし、切り分けを短くするための“境目”を先に列挙する。
結論: nullが入り込みやすい混入点は限られる。境目が前に寄るほど、切り分けが短くなる。
nullが入り込みやすいのは、だいたいここ。
- 設定/環境変数(取得直後)
- DB/ファイル/ネットワーク(IO直後)
- UIイベント(UI値取得直後)
- 外部APIレスポンス(受信直後)
- DI注入(生成直後)
止める場所が前に寄るほど、切り分けが短くなる。
逆に、内部ロジックへnullが入り込むほど、null判定が散って仕様が濁る。
7. Guardの型(最小テンプレ)
狙い: Guardを「構文」ではなく「境目で契約を決める小さな関数群」として定義し、貼る場所と効き所が迷子にならない状態へ寄せる。
結論: Guardは予約語でも特別な構文でもない。境目で必須条件を判定して止めるための最小ユーティリティ(所作+型)を指す。
GuardはC#の予約語でも特別な構文でもない。
このページでのGuardは、設定/IO/UI/DI/外部API受領など “外から入る値” を受け取った直後 に、必須条件を判定して止めるための 最小ユーティリティ(所作+型) を指す。
Guardが担う役割は次の3つ。
- 境目で必須条件を判定する(null/空/空白など)
- 条件を満たさない場合、その場で例外(またはTry)として止める(落ち方を「境目」に寄せる)
- 条件を満たす場合、以降を
stringなど non-null 前提で進められる形に寄せる(null判定を後段へ持ち込まない)
テンプレは「貼って終わり」にするため、用途コメント込みで置く。
7-1. 最小:必須stringを止める(空/空白も含む)
狙い: string? を境目で止めて string に寄せ、後段のnull判定を増やさない。
public static class Guard
{
// 必須文字列: null/空/空白なら止める(設定値・入力値の必須チェック用)
public static string NotBlank(string? value, string name)
=> !string.IsNullOrWhiteSpace(value)
? value
: throw new InvalidOperationException($"{name} is missing.");
}
// 使い方: 境目(設定取得直後)で止め、以降は string 前提で進める
var apiKey = Guard.NotBlank(config["ApiKey"], "ApiKey");
ポイント:
- nullだけでなく「空/空白」も同じ扱いに寄せると、後段の条件が減ることが多い
- 例外種別は運用に合わせる(入力なら
ArgumentException、設定ならInvalidOperationExceptionが馴染みやすい)
7-2. .NET 6+(.NET 8含む):引数nullは即止める
狙い: 引数境界で止め、以降の処理をnon-null前提へ寄せる。
public sealed class UserService
{
public void UpdateName(string userId, string name)
{
// 参照型引数がnullなら止める(.NET 6+ の定番)
ArgumentNullException.ThrowIfNull(userId);
ArgumentNullException.ThrowIfNull(name);
// 追加の契約(空/空白など)も、ここでまとめて決める
if (userId.Length == 0) throw new ArgumentException("userId is empty.", nameof(userId));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name is blank.", nameof(name));
// ここから先は non-null 前提(nullチェックを撒かない)
}
}
7-3. .NET Framework 4.8:同じ考え方を手書きで寄せる
狙い: ThrowIfNull が無い環境でも“境目で止める”を同じ型に寄せる。
public sealed class UserService
{
public void UpdateName(string userId, string name)
{
// 参照型引数がnullなら止める(ThrowIfNull が無い環境の書き方)
if (userId is null) throw new ArgumentNullException(nameof(userId));
if (name is null) throw new ArgumentNullException(nameof(name));
// 追加の契約(空/空白など)も、ここでまとめて決める
if (userId.Length == 0) throw new ArgumentException("userId is empty.", nameof(userId));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name is blank.", nameof(name));
}
}
8. !(null許諾)の扱い
狙い: 警告消しのための ! を増殖させず、根拠をコードへ寄せる。
結論: ! は「ここはnullにならない」と言い張る道具。根拠がない場所で使うと、警告は消えるが原因は残る。
悪い例:
var apiKey = config["ApiKey"]!;
直す(根拠をコード化する):
var apiKey = config["ApiKey"];
if (string.IsNullOrWhiteSpace(apiKey))
throw new InvalidOperationException("ApiKey is missing.");
! を使うなら最低条件を決める。
- 根拠がコードで担保されている(直前でGuard済み等)
- その行が「契約の境目」になっている(散らさない)
- 警告消しのために増殖させない
9. 禁書庫A(早見):契約パターンの選び方
狙い: 「nullになり得る」に遭遇した瞬間に、返し方を例外/Try/nullable/既定値へ寄せる。
結論: 「例外」「Try」「nullable」「既定値」のどれかに寄せると、全体の読み筋が立つ。
「nullになり得る」状態に遭遇したとき、どう返すかを先に決める。
| 状況 | 契約(推奨) | 返し方 | メリット | 注意点 |
|---|---|---|---|---|
| 必須が欠けた | 例外 |
ArgumentException / InvalidOperationException
|
失敗が明確 | メッセージに根拠を残す |
| 見つからないが起き得る | Try | bool TryGetXxx(..., out T value) |
分岐が揃う |
outへdefaultを入れる |
| 任意値が仕様 | nullable | T? |
型で表せる | 呼び出し側へ責務が移る |
| 既定値が仕様 | 既定値 | ?? defaultValue |
分岐が減る | 意味が崩れやすい |
混ざると、nullが流れ続けて後段の判定が増える。
10. 判例:混入経路が多い順に潰す(設定/DI/LINQ/as/Dictionary/UI/イベント)
狙い: 混入点の近くで止める形へ寄せ、例外が表に出る場所の揺れを抑える。
10-1. 設定取得直後:欠けている/空
狙い: 設定の欠けを取得直後で止めると、後段での例外位置が揺れにくい。
悪い例:
var apiKey = config["ApiKey"];
Call(apiKey);
直す:
var apiKey = Guard.NotBlank(config["ApiKey"], "ApiKey");
Call(apiKey);
ポイント:
- 止める場所は「設定取得直後」で揃える
- 必須は
?? ""等で曖昧にせず例外へ寄せる - 例外メッセージは “何が欠けたか” を残す
10-2. 生成直後(DI含む):必須依存がnull
狙い: 生成直後で契約を決め、以降は non-null 前提へ寄せて扱いを揃える。
悪い例:
public sealed class Worker
{
private readonly ILogger _log;
public Worker(ILogger log)
{
_log = log;
}
}
直す:
public sealed class Worker
{
private readonly ILogger _log;
public Worker(ILogger log)
{
ArgumentNullException.ThrowIfNull(log); // 依存は生成直後で止める
_log = log;
}
}
ポイント:
- DIの必須依存は生成直後で止める
- “後で落ちる”状態を作らず、境目で例外へ寄せる
- 以降はnon-null前提(null判定を散らさない)
10-3. LINQ:FirstOrDefault が空を返す
狙い: LINQの「空」は静かに流れやすいので、見つからない時の扱いをその場で確定させる。
悪い例:
var user = users.FirstOrDefault((x) => x.Id == id);
return user.Name;
直す(空の扱いを決める):
var user = users.FirstOrDefault((x) => x.Id == id)
?? throw new InvalidOperationException($"User not found. id={id}");
return user.Name;
ポイント:
-
FirstOrDefaultの空は例外/Try/既定値のどれかへ寄せる -
?? throwは“境目で止める”意図を短く書ける - 後段で
user?を連鎖させない
10-4. as キャスト:静かにnullになる
狙い: as の失敗はnullとして流れるので、型が要件ならその場で止める。
悪い例:
var view = obj as IView;
view.Show();
直す:
if (obj is not IView view)
throw new InvalidOperationException("IView is required.");
view.Show();
ポイント:
-
asは失敗しても例外にならずnullとして流れる - 型が必須なら
is notで境目を作り例外へ寄せる - nullを後段へ持ち込まず、切り分けを短くする
10-5. Dictionary:添字アクセス
狙い: 「無い」を例外で落とすなら、メッセージを厚くして原因が追える形へ寄せる。
悪い例:
var v = map[key];
直す:
if (!map.TryGetValue(key, out var v))
throw new KeyNotFoundException($"key not found: {key}");
ポイント:
- 添字アクセスは“無い”が例外で表に出るが情報が薄くなりがち
-
TryGetValueで分岐を揃え、必要なら例外メッセージを厚くする - 返し方は Try/例外のどちらかへ寄せる
10-6. UI値取得直後(WinForms):Value がnull
狙い: UIは未選択/型違い/nullが混ざりやすいので、取得直後で止めて業務側へ持ち込まない。
悪い例:
var text = dataGridView1.CurrentCell.Value.ToString();
直す(UI値取得直後で止める):
var cell = dataGridView1.CurrentCell;
if (cell is null) return;
var value = cell.Value;
if (value is null) return;
var text = value.ToString();
ポイント:
- UIは“未選択/未入力”が混ざる前提で境目を作る
-
CurrentCellとValueの2段で止める - 業務側へ
nullを持ち込まない
10-7. イベント:sender が想定外
狙い: sender は想定型で来ない経路があるので、型チェックで境目を作る。
悪い例:
void OnClick(object sender, EventArgs e)
{
var btn = (Button)sender;
btn.Text = "x";
}
直す:
void OnClick(object? sender, EventArgs e)
{
if (sender is not Button btn) return;
btn.Text = "x";
}
ポイント:
-
senderは想定型で来る保証が弱い -
object?と型チェックで境目を作る - キャスト例外/NullReferenceExceptionの混入点を早める
11. ?. と ?? の使い所
狙い: nullを「通す/止める/置き換える」を短く書きつつ、nullable連鎖を増やさない。
?. と ?? は、null を「通す/止める/置き換える」を短く書くための演算子。
ただし便利さの反面、型が nullable 側へ寄るので、雑に使うと後段へ ? が連鎖しやすい。
11-1. ?.(null 条件演算子)は何か
狙い: 左がnullのときに評価を止め、結果をnullへ寄せる仕組みを先に揃える。
?. は「左が null なら、右側(プロパティ/メソッド呼び出し/添字)を評価せず、結果は null にする」。
-
obj?.Prop:objが null ならnull、そうでなければobj.Prop -
obj?.Method():objが null なら呼ばない(副作用も起きない) -
obj?[i]: 添字版(indexer)
例(参照型):
string? name = user?.Name; // user が null なら name は null
例(値型へ変換される):
int? len = user?.Name?.Length; // Length は int だが、途中に null があるので int? になる
注意:
-
?.を挟むと、結果がT?(nullable)側へ寄る
→stringではなくstring?、intではなくint?が増えやすい
11-2. ??(null 合体演算子)は何か
狙い: 既定値へ置き換える構文を先に揃え、意図が曖昧にならない状態へ寄せる。
?? は「左が null なら右を採用する」。
-
left ?? right:leftが null でなければleft、null ならright - 右側は 必要なときだけ評価される(遅延評価)
string name = user?.Name ?? "(unknown)"; // Name が null のときだけ "(unknown)"
int len = user?.Name?.Length ?? 0; // int? を int に戻す
11-3. ??=(null 合体代入)は何か
狙い: null のときだけ代入する構文を先に揃え、初期化の意図を短くする。
x ??= y は「x が null のときだけ y を代入する」。
_cache ??= new Dictionary<string, string>();
11-4. 使い所(使うと整理されるパターン)
狙い: “仕様として通す/置き換える/止める”のどれかへ寄せ、nullable連鎖を増やさない。
(A) 任意値として null を通すのが仕様
例: 表示項目が無いなら空欄で良い、ログの追加情報が無いなら省略で良い、など。
var detail = user?.Profile?.Detail; // 無いなら null のまま
(B) 既定値が仕様(null を “意味のある値” へ置き換える)
例: 無いなら 0、無いなら false、無いなら "(unknown)"。
var displayName = user?.Name ?? "(unknown)";
var timeoutMs = settings?.TimeoutMs ?? 10_000;
(C) イベント呼び出し(購読が無いなら何もしない)
これは ?.Invoke が読みやすい。
Updated?.Invoke(this, EventArgs.Empty);
(D) 「無いなら止める」を式で書く(?? throw)
境目で止める意図が明確になる。
var user = users.FirstOrDefault((x) => x.Id == id)
?? throw new InvalidOperationException($"User not found. id={id}");
11-5. 使い所ではない(ここは Guard へ寄せる)
狙い: 必須値の欠けを隠さず、取得直後で止める形へ寄せる。
必須値 を ?. / ?? で曖昧にすると、欠けた事実が隠れて後段で詰まりやすい。
必須は「入ってきた直後」で止める(Guard)に寄せる。
// 必須のつもりで ?? "" に寄せると、欠けた事実が消える(空文字が混ざる)
var apiKey1 = config["ApiKey"] ?? "";
// 必須は取得直後で止める(欠けている事実を残す)
var apiKey2 = Guard.NotBlank(config["ApiKey"], "ApiKey");
12. チェックリスト(レビューで見る所)
狙い: 見落としやすい混入点を短い観点に落とし、指摘の揺れを減らす。
-
<Nullable>enable</Nullable>が入っている -
string/string?が契約として一貫している - 設定取得直後/IO直後/UI値取得直後でGuardしている
-
!が散っていない(根拠が読める一点へ寄っている) -
FirstOrDefault/as/ Dictionary添字 / UI値の扱いが揃っている - null判定が内部ロジックへ拡散していない
12-1. 最短チェック(持ち帰り用)
狙い: 有効化 / 契約 / 境目 / ! / 空の扱い / UIを揃える。
| 観点 | 確認ポイント | よくあるズレ | 見る場所 |
|---|---|---|---|
| 有効化 |
<Nullable>enable</Nullable> が入っている |
そもそも警告が出ない | csproj |
| 契約 |
string / string? が一貫している |
入出力で混在 | 引数/戻り値/フィールド |
| 境目 | 設定取得直後/IO直後/UI値取得直後でGuard | 深い場所で突然落ちる | 設定/IO/UIイベント |
! |
根拠が読める一点へ寄っている | 警告消しとして散る |
! の出現箇所 |
| 空の扱い |
FirstOrDefault 等の空を例外/Try/既定値へ寄せる |
放置してnullが流れる | LINQ/コレクション |
| UI |
Value のnull/型違いを境目で止める |
業務側へ持ち込む | WinForms |
| 拡散 | null判定が内部へ広がっていない | 条件が増えて仕様が濁る | 内部ロジック |
12-2. 判例表(OK/NG)
狙い: 揃えるべき差分を表にし、判断を短くする。
| 観点 | OK例 | NG例 | 理由(壊れ方) | レビューで見る所 |
|---|---|---|---|---|
| 契約の型 | 必須はstring、任意はstring?
|
何でもstringで受ける |
扱いの責務が散る | 引数/戻り値の注釈 |
| Guardの位置 | 入力直後で例外/Tryへ寄せる | 深い場所で突然落ちる | 切り分けが長引く | 設定/IO/UI直後 |
!の置き場 |
根拠がある一点へ寄せる | 警告消しとして多用 | 原因が隠れる |
!の出現箇所 |
| 空の扱い | 空を例外/Tryで扱う |
FirstOrDefault放置 |
nullが後段へ流れる | LINQ結果の扱い |
12-3. レビュー観点
狙い: 方向性だけを揃え、手戻りを短くする。
| 観点 | ありがちな見落とし | 壊れ方 | 指摘コメント例(直球禁止) |
|---|---|---|---|
| 注釈の整合 |
string/string?が混在 |
扱いの責務が揺れる | 入出力の注釈が揃っているか確認したい |
| Guard配置 | Guardが深い位置にある | 切り分けが長引く | 入力直後で止めた後はnon-null前提へ寄せたい |
! |
!が増える |
原因が隠れる |
!の根拠がコードから読める形へ寄せたい |
| LINQ | 空の扱いが曖昧 | nullが流れ込む | 空の扱いを例外/Tryで揃えたい |
| UI値 |
Valueのnullを見落とす |
再現が揺れる | UI値取得直後で止める形へ寄せたい |
13. セルフチェック(5問)
狙い: その場で判定できる問いにして、型/境目/演算子の混在を減らす。
- その
string?は「任意」か「境目で止め忘れ」か - その
!は「根拠」か「警告消し」か -
FirstOrDefaultの「空」は例外/Try/既定値のどれへ寄せるか -
asの失敗を null で流す設計にしていないか -
?./??で必須値を曖昧にしていないか(欠けた事実が残るか)
関連トピック
狙い: 起点になるIndexと、周辺ルールを必要な順に並べる。