連載Index(読む順・公開済(リンク)はここが最新): S00_門前の誓い_総合Index
文字列操作を押さえると、実装の“止まる所”が一気に減る。
知っていて損をしない知識なので、よく使う型は手元に揃えておくと強い。
まず一覧で全体像を掴み、次に最短テンプレ、最後に詰まりパターンとレビュー観点で固める。
よく使う文字列操作(一覧)
結論: 迷い所は「比較条件」「空要素」「置換範囲」「連結コスト」「Regexの上限」の5点に集約される。
| やりたいこと | 代表API | 迷い所(1行) |
|---|---|---|
| 検索/部分一致 | Contains / IndexOf / StartsWith / EndsWith |
StringComparison を明示するか |
| 比較 | Equals / Compare | Ordinal系かCulture系か |
| 置換 | Replace / Regex.Replace | 全置換か、範囲を限定するか |
| 分割 | Split | 空要素を残すか消すか、Trimするか |
| 結合 | string.Join / StringBuilder | ループ連結で割り当てが増えるか |
| 整形 | Trim / Normalize | 入力正規化の置き場 |
| 正規表現 | Regex(IsMatch/Match/Replace) | タイムアウトを入れるか |
| キー比較(コレクション) | Dictionary/HashSet + StringComparer | 比較方針を生成時に渡すか |
1. このページで手に入るもの(最短)
結論: 文字列操作を「比較条件」「空要素」「置換範囲」「連結コスト」「Regex安全性」の5点で揃える。
- コピペで使える:
NullRules/StringRules/KeyRules/SplitRules/ReplaceRules/JoinRules/RegexRules(用途コメント+★OK/★NG付き) -
StringComparisonとStringComparerの使い分けが理屈で言える -
Splitの空要素/トリム方針で列ズレを止められる - 「1箇所だけ置換」「先頭だけ置換」を範囲明示で書ける
- ループ連結の割り当て増加を説明できる(遅くなる理由が言える)
- Regexにタイムアウトを入れる判断ができる(固まりやすい形を避ける)
2. 先に逆引き(症状→原因→対策)
狙い: スクロールだけで「どこを直すか」が決まる状態へ寄せる。
| 症状 | ありがちな原因 | 切り分け(見る場所) | 最短の対処 | 再発防止(ルール化) |
|---|---|---|---|---|
| 端末/環境で一致判定が変わる | 比較条件が未指定(文化依存) | Contains/StartsWith/EndsWith/Equals/IndexOf |
StringComparison を明示 |
用途で比較方針を分ける |
| nullで落ちる/想定外になる | インスタンスメソッド呼び出し |
.Equals()/.Trim()/.Length
|
string.Equals / 起点で正規化 |
nullを境目で止める |
| 列がズレる/値が欠ける | 空要素/トリム方針が未決 | Split引数/オプション | 空要素の扱いを決める | 入力正規化地点を決める |
| 置換が広すぎて壊れる | 全置換を雑に当てる | Replace利用箇所 | 範囲を明示 | 置換意図をコードに残す |
| 連結が遅い/メモリが増える | ループで+連結 |
ループ内連結 | StringBuilder | ループ連結の規約化 |
| Regexで固まる/保守が止まる | 無制限Regex/読めない | Regex生成/パターン | タイムアウト/命名グループ | Regex採用基準を決める |
| キー大小無視が効かない | 既定Comparerのまま | Dictionary/HashSet生成 |
StringComparer 指定 |
キー比較方針を決める |
3. 最短テンプレ(コピペ)
狙い: 「何が悪い/何が起きる/なぜ良い」を短く言語化し、コードが説明の補助になる形へ寄せる。
3-1. nullで落ちる形を避ける(Equals/Trim/Length)
結論: 文字列の比較は string.Equals に寄せ、入力正規化は起点で止める。
なぜ悪い(具体):
-
sss.Equals("A")は 呼び出し先がnullなら即座に例外になる(比較のつもりが制御フローになる) - 例外が起きる場所が「比較箇所」になり、原因が文字列比較なのか入力なのかが曖昧になる
なぜ良い(具体):
-
string.Equals(a, b, ...)は nullを値として扱える(比較条件も1行で見える) - 起点で
Trim/null→""を済ませると、後段の条件分岐とログが安定する
using System;
public static class NullRules
{
// 用途: nullを含む可能性がある入力の比較
public static bool EqualsOrdinalIgnoreCase(string? a, string? b)
{
// ★NG: a.Equals(b) は a が null だと落ちる
// ★OK: string.Equals は null を許容し、比較条件も指定できる
return string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
}
public static string NormalizeUserInput(string? text)
{
// ★OK: 入力直後(起点)で null と空白を整える
// - null を空文字へ寄せる
// - 前後空白はTrimする
return (text ?? "").Trim();
}
}
3-2. 検索/比較は StringComparison を明示する
結論: 比較条件を明示すると、文化差とレビューの揺れが減る。
なぜ悪い(具体):
-
Contains(value)の既定挙動は 比較条件がコードから読めない(OrdinalなのかCultureなのかが不明) - 文化依存が混ざると、端末設定やOS環境で「一致/不一致」が揺れる形が残る
なぜ良い(具体):
-
StringComparison.Ordinal(…IgnoreCase)を指定すると 内部用途の比較が安定する(同じ入力なら同じ結果) -
CurrentCultureを選ぶ場面も「表示用」という意図が残り、選択理由を説明できる
using System;
public static class StringRules
{
// 用途: 内部ID/キー/プロトコル文字列の検索
// 方針: 文化依存を避け、比較条件を明示する
public static bool ContainsOrdinalIgnoreCase(string? text, string value)
{
// ★NG: text.Contains(value) は比較条件が見えない
// ★OK: OrdinalIgnoreCase は「内部用途の大小無視」に寄せやすい
return text?.Contains(value, StringComparison.OrdinalIgnoreCase) == true;
}
public static int IndexOfOrdinalIgnoreCase(string? text, string value)
{
// ★OK: 見つからないは -1(慣れた取り扱いへ寄せる)
return text?.IndexOf(value, StringComparison.OrdinalIgnoreCase) ?? -1;
}
}
3-3. コレクションのキー比較は StringComparer を生成時に渡す
結論: キー用途の大小無視は、値変換よりComparerで揃える。
なぜ悪い(具体):
- 値側で
.ToLower()などを使うと 変換用の新しいstringが毎回発生し、割り当てが増える - 変換の比較方針が散らばり、片側だけ変換されて「見つからない」形が出やすい
- 文化依存の大小変換が混ざると、内部キー用途で意図しない揺れが残る
なぜ良い(具体):
-
Dictionary/HashSetは Comparerでハッシュ計算と等価判定を行うため、生成時に渡すのが一番筋が良い - 変換用の中間文字列が減り、キー比較の方針が1点に寄る
using System;
using System.Collections.Generic;
public static class KeyRules
{
public static Dictionary<string, int> CreateMapForKey()
{
// ★NG: 既定Comparer。用途によっては大小無視が効かず詰まる
// var map = new Dictionary<string, int>();
// ★OK: キー用途の大小無視を生成時に決める
// - ハッシュ計算も等価判定もComparerで揃う
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
return map;
}
}
3-4. 分割は「空要素」と「トリム」を先に決める
結論: Splitは空要素が本体。列として扱うなら消さない、値リストなら消す。
なぜ悪い(具体):
-
RemoveEmptyEntriesを何も考えず付けると、"A,,C"の 空列が詰められて列番号がずれる - 列ズレは「Indexで拾っている値」が別物になり、後段のバリデーションや集計が壊れやすい
なぜ良い(具体):
- 「列用途/値用途」を先に決めると、空要素の扱いが説明できる(列なら残す、値なら消す)
-
TrimEntriesを入れると、入力側の空白差を吸収できる(.NET 6+)
using System;
public static class SplitRules
{
// 用途: "A, B ,C" のような「簡易」区切りを配列へ
// 注意: 引用("...")やエスケープ規約があるならCSVパーサへ寄せる
public static string[] ParseCsvLike(string text, bool keepEmptyColumns)
{
// ★OK: TrimEntries(.NET 6+)は入力の空白差を吸収できる
// ★NG: 空要素を消す/残すが未決だと列ズレが起きやすい
var options = StringSplitOptions.TrimEntries;
if (!keepEmptyColumns)
{
// 値リスト用途: 空要素は除外してよい
options |= StringSplitOptions.RemoveEmptyEntries;
}
else
{
// 列用途: 空要素も列として残す(RemoveEmptyEntriesは付けない)
}
return text.Split(',', options);
}
}
3-5. 置換は「どこを変えるか」をコードに残す
結論: Replaceは全置換。部分置換は範囲を明示する。
なぜ悪い(具体):
-
Replace(old, new)は 一致した全箇所が対象になり、「一部だけ」のつもりでも広範囲に反映される - 置換範囲が曖昧だと、「どこが変わる仕様か」を説明できず、レビューと調査が伸びる
なぜ良い(具体):
- 先頭だけ/1回だけ/範囲限定を明示すると、置換の意図がコードに残る
- 仕様変更時も「この置換はここまで」という境界が追える
using System;
public static class ReplaceRules
{
// 用途: 先頭だけ置換 / 最初の1回だけ置換
// 狙い: 置換範囲がコードに残り、レビューで追える形へ寄せる
public static string ReplacePrefix(string text, string prefix, string newPrefix)
{
// ★NG: text.Replace(prefix, newPrefix) は全箇所が対象になる
// ★OK: 先頭限定の意図がそのまま残る
return text.StartsWith(prefix, StringComparison.Ordinal)
? newPrefix + text[prefix.Length..]
: text;
}
public static string ReplaceFirst(string text, string oldValue, string newValue, StringComparison comparison)
{
// ★OK: 最初の1回だけ置換。どこを変えたかが明確になる
var index = text.IndexOf(oldValue, comparison);
if (index < 0) return text;
return text.Substring(0, index)
+ newValue
+ text.Substring(index + oldValue.Length);
}
}
大小無視の置換(最後の手段):
using System;
using System.Text.RegularExpressions;
public static class ReplaceIgnoreCaseRules
{
public static string ReplaceIgnoreCase(string src, string patternLiteral, string replacement)
{
// ★OK: IgnoreCase置換はRegexへ寄せる
// - パターンがリテラルなら Escape で意図しないメタ文字を防ぐ
// - 外部入力が混ざるならタイムアウトを入れる
var pattern = Regex.Escape(patternLiteral);
return Regex.Replace(
input: src,
pattern: pattern,
replacement: replacement,
options: RegexOptions.IgnoreCase | RegexOptions.CultureInvariant,
matchTimeout: TimeSpan.FromMilliseconds(200));
}
}
3-6. 結合は「配列はJoin」「ループはStringBuilder」
結論: ループ連結は割り当てとコピーが増えやすい。用途で道具を変える。
なぜ悪い(具体):
-
stringは不変。s += xは毎回 新しいstringを割り当て、旧stringの内容を 全コピーしてから追記する - 反復回数が増えるほどコピー総量が積み上がり、CPU時間が伸びる(サイズ増加でコピー量が増えるため)
- 中間文字列が大量に発生し、世代0/1の回収が増える。負荷が高い場面では待ちが出やすい
なぜ良い(具体):
-
StringBuilderは内部バッファへ追記し、最後にToString()で最終文字列を1回生成できる -
string.Joinは配列/リスト結合の意図が明確で、最終サイズをまとめて確保しやすい
using System;
using System.Text;
public static class JoinRules
{
// 用途: 配列/リストを区切って結合(1回で終わる)
public static string JoinComma(string[] values)
{
// ★OK: 配列結合の第一候補。読みやすく速い
// - 事前に最終サイズを見積もり、まとめて確保しやすい
return string.Join(",", values);
}
// 用途: ループで多量に連結(ログ行/CSV組み立て/バッファ生成)
public static string BuildCsv(int[] values)
{
// ★OK: ループ連結はStringBuilderへ寄せる
// - 中間文字列を大量に作らず、内部バッファへ追記する
var sb = new StringBuilder(capacity: 64);
for (int i = 0; i < values.Length; i++)
{
if (i > 0) sb.Append(',');
sb.Append(values[i]);
}
return sb.ToString();
}
}
補間(少数の連結なら可読性優先):
var id = 123;
var name = "Alice";
// ★OK: 少数なら読みやすさを優先してよい
var line = $"id={id}, name={name}";
3-7. Regexは「読みやすさ」と「タイムアウト」をセットにする
結論: 外部入力にRegexを当てるなら、固まりやすい形を避ける前提を入れる。
なぜ悪い(具体):
- Regexはパターンによって 最悪ケースで探索が爆発する形がある(バックトラックが増える)
- 外部入力に無制限Regexを当てると、実行が長引き「応答が返らない」状態が出やすい
- パターンが読めないと、仕様変更やバグ修正が止まりやすい
なぜ良い(具体):
-
matchTimeoutを入れると、最悪ケースを上限で止められる - 命名グループを使うと、抽出結果の意味がコードに残る
using System;
using System.Text.RegularExpressions;
public static class RegexRules
{
// 用途: 仕様が「パターン」で安定している入力の判定/抽出
// 方針: 1) 読める(命名グループ) 2) 固まらない(タイムアウト) をセットにする
public static bool TryExtractId(string input, out string id)
{
var re = new Regex(
pattern: @"^ID=(?<id>[A-Z]{3}-\d{3})$",
options: RegexOptions.CultureInvariant,
matchTimeout: TimeSpan.FromMilliseconds(200));
var m = re.Match(input);
if (!m.Success)
{
id = "";
return false;
}
// ★OK: 命名グループで意味を残す
id = m.Groups["id"].Value;
return true;
}
}
4. 解説:用語の定義
結論: 比較条件とキーComparerを最初に揃えると、環境差の詰まりが減る。
- 文化依存: 言語や地域設定により大文字小文字や並びの扱いが変わる要素
-
StringComparison.Ordinal: 文字のコード値で比較(内部ID/キー用途に寄せやすい) -
StringComparison.CurrentCulture: 現在カルチャに従う比較(表示用の自然さに寄せやすい) - 空要素:
"A,,C,"をSplit(',')したときに出る""の要素 -
StringComparer: Dictionary/HashSetのキー比較を決めるComparer(ハッシュ計算と等価判定に関与) - Regexタイムアウト: 最悪ケースで長く回り続ける形を避けるための上限
5. 解説:評価規則(迷ったときの基準)
結論: 迷う箇所は「用途」で分ける。内部用途はOrdinal、表示用途はCulture寄りへ。
5-1. 比較/検索の基準(StringComparison)
狙い: StringComparison の選択が毎回変わる形を減らす。
- 内部ID/キー/プロトコル:
Ordinal/OrdinalIgnoreCase - UI表示用の自然な並び/比較:
CurrentCulture/CurrentCultureIgnoreCase -
.ToLower()/.ToUpper()で揃える: 変換コストと仕様解釈が混ざりやすい(Comparer/Comparisonへ寄せる)
5-2. キー比較の基準(StringComparer)
狙い: 大小無視が必要な用途を「値変換」で処理しない形へ寄せる。
- キー用途:
StringComparer.OrdinalIgnoreCaseが第一候補 - 表示名等のキー:
StringComparer.CurrentCultureIgnoreCaseが必要な場面もある(用途が明確なときだけ)
5-3. 分割の基準(Split)
狙い: 空要素を消す/残すが揺れない形へ寄せる。
- 列用途(列数が意味を持つ): 空要素を残す(列として扱うなら空要素は消さない)
- 値リスト用途(値だけが欲しい): 空要素を消す
- トリム: 入力の空白差を吸収したいなら
TrimEntries(.NET 6+) - CSV: 引用規約があるならSplitから撤退する(パーサへ寄せる)
5-4. 置換の基準(Replace)
狙い: 置換範囲が不明な形を減らす。
- 全置換が仕様:
Replace - 先頭だけ/1回だけ/特定範囲:
IndexOf+ 範囲明示(ReplaceFirst等) - 大小無視の置換: Regexへ寄せるが、保守性と採用理由を先に確認する
5-5. 結合の基準(Join / StringBuilder)
狙い: 連結が遅い・メモリが増える理由を説明できる形へ寄せる。
- 配列/リスト結合:
string.Join - ループ連結:
StringBuilder - 補間: 少数の連結で可読性優先に寄せる
5-6. Regexの基準
狙い: 強い道具を「強いまま」使う形へ寄せる。
- 採用: 仕様がパターンとして安定、抽出が必要、手続きで書くと逆に読めない
- 回避:
StartsWith/Contains/Splitで十分、仕様変更が頻繁、パターンが肥大化しやすい - 外部入力: タイムアウト必須、命名グループで意味を残す
6. 落とし穴(詰まりやすい形)
結論: ここが残ると「環境差」「null」「列ズレ」「置換暴発」「負荷」「固まり」が繰り返されやすい。
-
Contains(value)/Equalsで比較条件が見えない - インスタンス呼び出しでnullが例外になる(比較が制御フローになる)
-
Splitで空要素を消す/残すが場面で揺れる -
Replaceで「一部だけ」のつもりが全置換になる - ループで
s += ...を続け、中間文字列が大量に発生する - Regexにタイムアウトがなく、外部入力で固まる形が残る
7. 禁書庫(決め打ちで読む)
結論: 迷ったらここへ戻すと、判断が短くなる。
禁書庫A: 逆引き(決め打ち)
- 内部用途の検索/比較:
Ordinal/OrdinalIgnoreCase - キー比較:
StringComparer.OrdinalIgnoreCaseを生成時に渡す - 列用途Split: 空要素を残す
- 値リストSplit: 空要素を消す
- 部分置換: 範囲明示(先頭/1回だけ)
- ループ連結: StringBuilder
- 外部入力Regex: タイムアウト必須
禁書庫B: 早見(代表API)
- 検索:
Contains(..., StringComparison)/IndexOf(..., StringComparison) >= 0 - 比較:
string.Equals(..., StringComparison) - 置換: 全置換は
Replace、部分置換はIndexOf+範囲明示 - 分割:
Splitは空要素とトリム方針が本体 - 結合: 配列は
string.Join、ループはStringBuilder - Regex: 命名グループ+タイムアウト
8. 判例:混入点が多い順に潰す
結論: 例を見て終わりにせず、「影響」「理由」「直した後の意味」が説明できる形へ寄せる。
8-1. 比較条件が見えない Contains
狙い: 比較条件未指定が残す影響を言語化し、直し方の理由が説明できる形へ寄せる。
何が起きて悪いか(具体):
-
Contains("abc")は比較条件が見えず、文化依存/大小の扱いが暗黙になる - 後から「内部IDなのに文化依存だった」「大小無視のつもりが一致しない」の議論が起きやすい
なぜ直しが良いか(具体):
-
StringComparisonを明示すると、比較方針がコード上の契約になる(レビューで揃う)
悪い例:
var text = "Abc-123";
// ★NG: 比較条件が見えない。文化依存/大小で揺れやすい
if (text.Contains("abc"))
{
}
直す:
var text = "Abc-123";
// ★OK: 内部用途の大小無視は OrdinalIgnoreCase へ寄せる
if (text.Contains("abc", StringComparison.OrdinalIgnoreCase))
{
}
ポイント:
- 比較条件がコードに残る
- 端末差を引き起こす要因が見える
8-2. Dictionaryのキー比較が既定のまま
狙い: 「大小無視が効かない」を、Comparerの仕組みで説明できる形へ寄せる。
何が起きて悪いか(具体):
-
Dictionary<string, ...>はComparerでハッシュと等価判定を行う - 既定Comparerのままだと、用途によっては
"abc"と"ABC"が別キー扱いになり「見つからない」が起きる - 値側で
.ToLower()に寄せると変換の割り当てが増え、変換漏れも混ざりやすい
なぜ直しが良いか(具体):
- 生成時に
StringComparer.OrdinalIgnoreCaseを渡すと、比較方針が1点に寄り、割り当ても減る
悪い例:
using System.Collections.Generic;
var map = new Dictionary<string, int>();
// ★NG: 既定Comparer。用途によっては大小無視が効かず詰まる
map["abc"] = 1;
var ok = map.ContainsKey("ABC"); // false になりやすい
直す:
using System.Collections.Generic;
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
// ★OK: キー用途の大小無視はComparerで決める
map["abc"] = 1;
var ok = map.ContainsKey("ABC"); // true
ポイント:
- 値変換よりComparerで揃えた方が責任が明確
- HashSetも同様に
StringComparerを渡す
8-3. Split の空要素で列がズレる
狙い: 空要素の扱いが未決だと、どんな壊れ方になるかを説明できる形へ寄せる。
何が起きて悪いか(具体):
- 列用途(位置が意味を持つ)で空要素を消すと、列番号が詰まり「別列の値を読み出す」形が起きる
- 例:
parts[1]が本来の列ではなくなり、後段のバリデーションや集計が壊れる
なぜ直しが良いか(具体):
- 列用途/値用途を分け、空要素を残す/消すを最初に決めると説明が揃う
悪い例:
var src = "A,,C,";
// ★NG: 値リスト用途のつもりで空要素を消すと、列用途でズレる
var parts = src.Split(',', StringSplitOptions.RemoveEmptyEntries);
直す:
var src = "A,,C,";
// ★OK: 列用途なら空要素も残す
var columns = src.Split(','); // ["A", "", "C", ""]
// ★OK: 値リスト用途なら空要素を消す
var values = src.Split(',', StringSplitOptions.RemoveEmptyEntries); // ["A", "C"]
ポイント:
- 列用途と値用途を混ぜない
- 引用規約があるCSVはSplitから撤退する(パーサへ寄せる)
8-4. 「最初の1回だけ」のつもりで全置換
狙い: 全置換が残す影響を説明でき、範囲明示の理由が言える形へ寄せる。
何が起きて悪いか(具体):
-
Replaceは全置換なので、「先頭だけ/1回だけ」のつもりでも想定外の箇所まで変わる - 変更範囲が説明できず、「どこが仕様か」が曖昧になりやすい
なぜ直しが良いか(具体):
- 範囲を明示すると、置換意図がコードに残り、仕様変更時に追いやすい
悪い例:
var src = "abc-abc-abc";
// ★NG: 全置換。1回だけのつもりだと想定外の箇所も変わる
var dst = src.Replace("abc", "XYZ");
直す:
var src = "abc-abc-abc";
// ★OK: 最初の1回だけ置換。範囲が明確
var dst = ReplaceRules.ReplaceFirst(src, "abc", "XYZ", StringComparison.Ordinal);
ポイント:
- 「どこを変えるか」がコードに残る
- レビューで「置換範囲」が議論できる
8-5. ループ連結で割り当てが増える
狙い: 遅くなる理由とメモリ増加の理由を、割り当て/コピー/GCで説明できる形へ寄せる。
何が起きて悪いか(具体):
-
stringは不変。s += xは毎回「新しいstring」を割り当て、旧stringの内容を丸ごとコピーする - 文字列が伸びるほどコピー量が増え、コピー総量が積み上がりやすい(反復が増えるほど重くなる)
- 中間文字列が大量に発生し、GC回収頻度が増える。負荷が高い場面では待ちが出やすい
なぜ直しが良いか(具体):
-
StringBuilderは内部バッファへ追記し、中間文字列を作らずに最後に1回だけ最終stringを作れる -
string.Joinは配列結合の意図が明確で、まとめて確保しやすい
悪い例(毎回新しい文字列が作られ、内容コピーが積み上がる):
var s = "";
// ★NG: ループで + は反復ごとに新しいstringを作る
// - 旧sの内容 + 追加分 を毎回コピーし直す
// - 反復回数が増えるとコピー総量が二次的に増えやすい
for (int i = 0; i < 10000; i++)
{
s += i.ToString();
}
直す(内部バッファへ追記し、割り当てとコピーを抑える):
using System.Text;
var sb = new StringBuilder(capacity: 256);
// ★OK: StringBuilderは内部バッファへ追記する
// - 中間文字列を大量に作らない
// - まとめてToString()で最終stringを1回作る
for (int i = 0; i < 10000; i++)
{
sb.Append(i);
}
var s = sb.ToString();
ポイント:
- ループ連結は「割り当て回数」と「コピー総量」が増えやすい
- 中間文字列が増えるとGCが増え、スループットもレイテンシも悪化しやすい
- 配列結合は
string.Joinが第一候補(意図が明確でまとめて確保しやすい)
8-6. Regexが固まる形が残る
狙い: 最悪ケースの挙動を説明でき、タイムアウトの理由が言える形へ寄せる。
何が起きて悪いか(具体):
- パターン次第でバックトラックが増え、最悪ケースで長く回り続ける形がある
- 外部入力に無制限Regexを当てると、応答が返らない状態が出やすい
なぜ直しが良いか(具体):
- タイムアウトで最悪ケースに上限を入れ、固まりを避けられる
- 命名グループで「何を抜いたか」を説明できる
悪い例:
using System.Text.RegularExpressions;
var re = new Regex(@"(a+)+$");
// ★NG: タイムアウト無し。最悪ケースで長く回り続ける形が残る
var ok = re.IsMatch("aaaaaaaaaaaaaaaaaaaa!");
直す:
using System;
using System.Text.RegularExpressions;
var re = new Regex(
pattern: @"(a+)+$",
options: RegexOptions.CultureInvariant,
matchTimeout: TimeSpan.FromMilliseconds(200));
// ★OK: タイムアウトで最悪ケースを避ける
var ok = re.IsMatch("aaaaaaaaaaaaaaaaaaaa!");
ポイント:
- 外部入力はタイムアウト前提
- 命名グループで意味を残すと保守が止まりにくい
9. チェックリスト:レビューで見る所
結論: 表を通すだけで、詰まりの混入が減る。
| 観点 | OK | NG | 見る場所 |
|---|---|---|---|
| 比較条件 |
StringComparison 明示 |
未指定 | Contains/Equals/StartsWith/IndexOf |
| null耐性 |
string.Equals/起点で正規化 |
インスタンス呼び出し | Equals/Trim/Length |
| キー比較 |
StringComparer 指定 |
既定Comparer | Dictionary/HashSet生成 |
| Split方針 | 空要素/トリム方針が明確 | 場面で揺れる | Splitの引数/オプション |
| Replace範囲 | 部分置換は範囲明示 | 全置換が混入 | Replace/IndexOf/Substring |
| 連結手段 | Join/Builderを用途で選ぶ | ループで+
|
ループ/集計処理 |
| Regex安全性 | タイムアウト/意味が読める | 無制限/肥大化 | Regex生成/パターン |
10. セルフチェック(5問)
結論: 5問に答えられると、迷い所が短くなる。
- 内部IDの比較で選ぶ
StringComparisonを理屈で言えるか - Dictionaryのキー比較で
StringComparerを渡す理由を言えるか -
Splitで空要素を消す/残すの判断基準を言えるか - 「最初の1回だけ置換」を
Replaceで書かない理由を言えるか - ループ連結で
+が遅くなりやすい理由を、割り当て/コピー/GCで説明できるか
回答例
- 内部ID/キー用途は
Ordinal/OrdinalIgnoreCaseに寄せやすい(文化依存を避ける) - DictionaryはComparerでハッシュ計算と等価判定を行う。生成時に渡すと方針が揃い、値変換の割り当ても減る
- 列用途は空要素を残す、値リスト用途は空要素を消す
-
Replaceは全置換。部分置換は範囲を明示した方が意図が残る -
stringは不変で、s += xは毎回新規文字列を割り当て、内容コピーが積み上がり、GCも増えやすい
関連トピック
- シリーズ総合Index(読む順・公開済リンクが最新)
- R03 【掟・判例】文字列比較の掟 StringComparisonを明示して迷いを消す
- R05 【掟・判例】ログ設計の掟(証拠を残す / 構造化ログ)