連載Index(読む順・公開済(リンク)はここが最新): S00_門前の誓い_総合Index
最初は意図があり、未使用警告を消せる喜びを覚える。
戻り値は要らない。outも要らない。だから _ を置きたくなる。
便利で、手軽で中毒になりそうになる。
ただ、中毒になり油断して _ が増えてくると話が変わる。
同じ記号が「捨てる(discard)」「変数名」「それ以外」に化け、意図が読めなくなる。
「捨てて良い値」と「捨てたら切り分けが遅れる値」まで同じ _ に並び、後から見直すと判断がつかない。
このページは、_ を置く場面と置かない場面を、戻り値/out/タプル/パターンで線引きする。
このページのゴール
- _ が何を意味するか(捨てる/変数名/パターン)を先に切り分ける
- 「捨てて良い値/捨てると困る値」を判例で明確化する
- 戻り値とoutのどちらを主に読むか、判断基準を揃える
- 使いどころ(out/タプル/パターン/_ = 呼び出し)を同じルールで扱う
- 現場の指摘が感情論にならないコメント例まで落とす
先に揃える: _ は「捨てる」「変数名」「パターン」で別物
1) discard(捨てる)としての _
「この値は使わない」をコード上で明文化するための _。
未使用警告を消す目的だけでなく、読み手に意図を残す用途になる。
- outの受け取りで捨てる(= out discard)
- 分解代入(タプル/Deconstruct)で一部を捨てる
- 戻り値を捨てる(_ = 呼び出し)
2) 識別子(変数名)としての _
_ はC#の識別子として有効な名前でもある。
そのため「捨てるつもりの _」が、ただの変数になって意味がねじれる事故が起きる。
- _ がdiscardとして扱われるのは、スコープ内に _ という識別子が存在しない場合
- _ を通常の変数名として宣言すると、以降の _ はdiscardではなく「その変数」になる
// これはdiscard(捨てる)として読める
_ = GetValue();
// ここで _ を変数として宣言すると、以降はdiscardではなく「変数_」になる
var _ = 123;
_ = GetValue(); // 「捨てる」ではなく「変数_に代入」になる
このページでは、運用として _ を変数名に使わない前提で話を進める(混ざると読みが壊れるため)。
3) パターンの _ (discard pattern)
パターンの _ は「それ以外」を表す分岐の記号で、discard変数でも変数名でもない。
スコープに _ があっても、パターンの _ は「既存の変数 _」にはならない。
string kind = token switch
{
Token.Number => "num",
_ => "other",
};
用語を最短で明文化する(読むために必要な分だけ)
-
タプル: 複数の値をまとめて返す戻り値の形。例:
(bool ok, string message)
var (ok, message) = ...;のように受け取り側で分解して使う。 -
パターン: switchやisで「形で分岐」する書き方。例:
x switch { 0 => ..., _ => ... }
ここの_は「それ以外」という分岐の記号で、変数ではない。
再発防止の掟: _ で捨てる基準
- 「この値は使わない」を明文化したい場面のみ _ を使う
なぜ: 読み手に意図が伝わり、未使用警告の抑止にも寄るため - 捨てると判断が遅れる値(原因切り分けに必要な値)は捨てない
なぜ: 事故時に「取っておけば分かった」が一番高く付くため - 戻り値とoutの両方があるAPIは、どちらを主に読むかを先に決める
なぜ: 呼び出し側が場当たりになると、捨て方が揺れて議論が止まるため - _ を変数名として運用しない
なぜ: discardとして読む前提が崩れ、コードの意味がねじれるため - 使わないoutが常態化するなら、呼び出し側で誤魔化さず設計を見直す
なぜ: 「本当は必要な情報」を捨てる運用に寄り、障害時に情報不足が起きるため
最短チェックリスト(読みが止まった時に見る所)
- _ が「変数として宣言」されていないか(スコープ汚染の有無)
- _ の位置はどれか(out/分解代入/_ = 呼び出し/パターン)
- 捨てた値は「成否」「分岐」「原因切り分け」に必要ではないか
- 捨てた値を保持すると、ログや例外で証拠が残せるか
- 同じAPIの呼び出しで、戻り値とoutの主役が揺れていないか
採用判断: どの値を捨てるか
1) outだけ捨てたい(out discard)
戻り値だけが必要で、outは使わない場面に合う。
// 例: 戻り値(int)だけ必要で、outのモデルは不要
int code = GetStatusCode(out Model _);
2) 戻り値だけ捨てたい(_ = 呼び出し)
呼び出し自体に意味があり、戻り値は不要な場面に合う(副作用やウォームアップなど)。
戻り値を捨てる意思表示を残す目的で使う。
// 例: キャッシュを温めるだけで戻り値は不要
_ = WarmUpCache();
WarmUpCache();だけの行は「何のために呼んでいるか」が残りにくい。副作用の見落としが起きやすくなる。
3) タプル/分解代入で一部だけ捨てたい
「要素の意味が曖昧」なまま捨てると、失敗時の切り分けが遅れる。
特に成否やステータスを捨てると事故になりやすい。
// 役割がある要素は保持する(成否は捨てない)
var (ok, message) = TryGetMessage();
// これは危険になりやすい(成否を捨てると失敗時に読めない)
var (_, message2) = TryGetMessage();
4) パターンで「それ以外」を表したい
パターンの _ は「変数名」でも「discard変数」でもなく、分岐の意図が明確になる。
bool enabled = state switch
{
State.Ready => true,
_ => false,
};
5) outと戻り値の「どちらが主役か」を揃える
同じAPIを複数箇所で使う場合、主役が揺れると読みが止まりやすい。
典型はTry系(outが主役)と、戻り値が主役の関数が混ざるケース。
// Try系: 成功判定が主役、outは成功時の値
bool ok = int.TryParse(text, out int value);
// 戻り値が主役: 戻り値が意味を持つなら out は補助
int result = GetResult(out Model model);
「戻り値を読むのかoutを読むのか」を決めると、_ の使いどころも揃えられる。
判例(OK/NG)
| 観点 | OK例 | NG例 | 理由(事故) | 見る所(確認ポイント) |
|---|---|---|---|---|
| outを使わない意思表示 | int code = GetStatusCode(out Model _); |
int code = GetStatusCode(out Model m); // m未使用 |
未使用変数が増え、意図も曖昧になる | out値が本当に不要か |
| 戻り値を捨てる意思表示 | _ = WarmUpCache(); |
WarmUpCache();の乱発で意図が不明 |
呼ぶ理由が読めないと副作用の見落としが起きる | 呼び出し自体に意味があるか |
| 捨てると困る値を保持 | var (ok, message) = TryGetMessage(); |
var (_, message) = TryGetMessage(); |
成功判定を捨てると失敗時に切り分けが遅れる | 捨てた値が判断に必要か |
| _ を変数名にしない | var unused = 0; |
var _ = 0; |
discardと混ざり、読みが壊れる | _ を通常の識別子にしていないか |
| outを捨てるより設計見直し | bool ok = TryGet(out value); |
int code = Get(out _);が多発 |
使わないoutが常態化するとAPI設計が歪む | outに意味がある設計か |
容赦なき断罪: 現場の指摘観点
止まるのは _ そのものではなく「捨てた理由が読めない」時。
未使用警告を消しただけの _ は、後から読む人にとって情報欠落になる。
特に障害対応では「捨てた値」が切り分けの鍵になり、調査時間が伸びる。
| 観点 | ありがちな見落とし | 困ること(現場の損失) | 指摘コメント例(直球禁止) |
|---|---|---|---|
| 捨てて良い値か | 何となく _ で捨てる | 障害時に情報不足で切り分けが遅れる | 捨てた値が切り分け材料になりそうなので、保持した方が調査コストが下がりそう |
| outと戻り値の主役 | 呼び出しごとに主役が揺れる | 読む側が毎回仕様を推測し、議論が止まる | このAPIは戻り値(out)が主役に見えるため、使い方を揃えると読みが止まりにくい |
| _ を変数名にしている | discardのつもりで導入 | 以降の _ が「捨てる」なのか「変数」なのか分からなくなる | _ を識別子として使うとdiscardの意味が崩れるため、別名に寄せたい |
_ = 呼び出し の意図 |
呼ぶだけの行が増える | 副作用の見落としや二重呼びが起きる | 戻り値を捨てて呼ぶ理由が重要に見えるため、目的が伝わる形に寄せたい |
| タプルで判定を捨てる | 成否(ok)を _ で捨てる | 失敗しても気づけず、後段で別の例外として爆発する | 成否が後段の前提に見えるため、判定を保持すると読みが安定しそう |
禁書庫A: 逆引き(症状→原因→対策)
| 症状 | ありがちな原因 | 切り分け(見る場所) | 最短の対処 | 再発防止(ルール化) |
|---|---|---|---|---|
| _ を見て読むのが止まる | discardと変数名の区別がない | _ が宣言されていないか | _ を変数名に使う箇所を排除 | _ はdiscard専用にする |
| outの未使用警告が多い | outを受けて捨てている | out引数の用途 | out discardへ寄せて意図を明示 | outが不要ならAPI役割を見直す |
| 戻り値を捨てて良いか迷う | 呼び出し理由が不明 | 呼び出しが副作用目的か |
_ =で捨てる意図を明示 |
「副作用目的のみ _ =」を運用化 |
| タプルの _ が読めない | 要素の意味が曖昧 | タプル要素名/順序 | 役割が必要な要素は捨てない | 捨てるのは判断不要な要素のみ |
| 事故調査で情報不足 | 捨てた値が判断材料だった | ログ/分岐条件 | 捨てずに保持してログへ回す | 「切り分け値は捨てない」を規約化 |
禁書庫B: チートシート(決め打ちで読む)
| 状況 | 形 | 判断 |
|---|---|---|
| outが不要 | Get(out Type _) |
使わない意思表示として通す |
| 呼ぶこと自体が目的 | _ = WarmUp(); |
意図が伝わるなら通す |
| 成否/分岐に必要 |
(_, value)の形 |
捨てない(判定材料は保持) |
| _ を変数名にしている | var _ = ... |
運用上NG(意味が崩れる) |
| 使わないoutが常態化 |
Get(out _)が多発 |
API役割の見直し候補 |
.NET Framework 4.8での扱い(差分が出る所だけ)
- _ をdiscardとして使う構文はC# 7.0以降の機能になる
.NET Framework 4.8でも、プロジェクトのC#言語バージョンが古いと構文が使えない - 差はランタイムではなく、コンパイラ(言語バージョン)側で出る
同じ.NET Framework 4.8でも、開発環境やLangVersionで可否が変わる
関連トピック
- 連載Index(読む順/公開済リンク): S00_門前の誓い_総合Index
- 省略(短縮)の判断基準を揃える系: R02 varの使い分け基準