連載Index(読む順・公開済(リンク)はここが最新): S00_門前の誓い_総合Index
多くのコードは、文字列比較にStringComparisonを付けずに動いている。
困らない理由は単純で、比較箇所が少ない間は == や StartsWith() の“いつもの挙動”で足りる。
ただ、検索・フィルタ・キー比較が増えて比較が散ると、「大小を無視するのか」「Culture(カルチャ)に従うのか」がコードから読めない比較が混ざりやすい。
その状態になると、ルールが揃っているかの確認が面倒になる。
StringComparisonは比較規則を引数で明示する、.NET標準のenum。
必要な場面だけに入れると、比較の意味がコード上に残り、迷いにくい。
このページのゴール
- StringComparisonの存在を知り、説明できる
- 文字列比較の判断基準(OrdinalかCultureか)を迷わず選べる
- Equals/Contains/StartsWith/EndsWith/IndexOfで比較規則を明示できる
- Dictionary/HashSetのキー比較をStringComparerで揃えられることを知る
- 既定動作の混在をレビューで見つけられる
- (.NET Framework 4.8) 差分がある箇所だけ、代替の形を持てる
いつから使えるのか(なぜ見たことがないのか)
- StringComparison自体は昔からある。.NET Framework 4.8にも存在する(列挙体として使える)。
- 一方で「よく使うAPIが、比較基準を引数で受け取れる」形は後から揃ってきた。
代表例がstring.Contains(value, StringComparison)で、これは.NET Core 2.1+で追加され、.NET Framework 4.8には無い。
(列挙体が無いのではなく、Contains側のoverloadが無い) - Framework系の現場では
IndexOf(. , StringComparison) >= 0が定番になり、StringComparisonが表に出づらい。 - これはC#の新文法ではなく、.NET標準ライブラリが用意している比較指定の使い分けの話になる。自作は不要で「引数で指定するだけ」が基本。
今回の登場人物(StringComparison / StringComparer / Culture)
- StringComparison: その1回の比較の規則を指定するenum(一致判定/検索向け)
- StringComparer: コレクションのキー比較規則をコンテナ側に持たせる比較器
-
Culture:
CultureInfo(CurrentCulture/InvariantCulture)を指す用語。.NETの用語としてはCultureで扱う方が混乱が少ない
StringComparisonの主要メンバと効果(一覧)
「何を指定すると、どう比べるのか」を短く固定する。
実務で出番があるのは、基本的にこの8つで足りる。
| メンバ | 大小文字 | Cultureの影響 | どう効く(比較の意味) | 主な用途 |
|---|---|---|---|---|
| Ordinal | 区別する | 受けない | 文字コード(ordinal)の一致で比べる | 識別子/キー/コード値/パス/設定キー |
| OrdinalIgnoreCase | 無視する | 受けない | 文字コードで比べつつ大小だけ無視する | 識別子/キーの大小揺れ吸収 |
| InvariantCulture | 区別する | Invariant | 文化圏に依存しないCulture規則で比べる | 文化圏差を避けたい言語系比較 |
| InvariantCultureIgnoreCase | 無視する | Invariant | InvariantCultureで大小も無視する | 文化圏差を避けたい検索/比較 |
| CurrentCulture | 区別する | CurrentCulture | その端末のCulture規則で比べる | UI文言/ユーザー入力を言語として扱う比較 |
| CurrentCultureIgnoreCase | 無視する | CurrentCulture | CurrentCultureで大小も無視する | UI検索/表示寄りの比較(要件がある場合) |
| StringComparison.CurrentCulture(=既定になる場面がある) | 場合による | 受ける | 既定に任せると意図が読みづらい | 既定比較の混在が起きやすい |
| (補足) 既定動作 | APIごとに異なる | 異なる | 「何基準か」が呼び出しから読めない | 後から揃えるのが面倒になる |
※ 上の「補足2行」はenumのメンバではなく、既定比較が混ざると困るという注意喚起のための枠。
先に切り分ける: 一致判定と並べ替えは別物
- 一致判定:
Equals/==/Contains/StartsWith/EndsWith/IndexOf - 並べ替え:
Compare/CompareTo/Sort/OrderBy
同じ「比べる」でも目的が違う。
このページは一致判定と検索を主役にし、「比較の意図がコードから読める」状態を優先する。
掟: 迷いが出る比較にはStringComparisonを明示する
- 文字列比較(一致判定/検索)は、意図が曖昧になりそうなら
StringComparisonを渡す(原則) - 識別子・キー・パスなど「言語の意味が要らない比較」は Ordinal 系を基本にする
- UI文言など「言語の規則が要る比較」は Culture(CurrentCulture/InvariantCulture) 系を選び、意図をコード上に残す
- コレクションのキー比較は
StringComparerを生成時に渡す
どう効くのか(困り方→メリットが見える形)
比較メソッドには「既定の比較規則」があるが、APIごとに性質が揃っていない。
既定に任せた呼び出しが混ざるほど、「この比較は何を狙っているか」がコードから読み取りにくい。
引数で明示すると、レビューで意図が見える。確認する範囲も絞りやすい。
例1: Dictionaryのキーが取れない(大小文字が混ざる)
設定キーや外部入力が絡むと、大小文字が混ざるのは珍しくない。
Dictionaryの既定は大小文字を区別するため、「入れたのに取れない」が起きる。
var map = new Dictionary<string, string>();
map["UserId"] = "123";
// KeyNotFoundExceptionになり得る
var v = map["userid"];
生成時に規則を渡すと、意図がブレない。
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
map["UserId"] = "123";
var v = map["userid"]; // 取れる
例2: フィルタ条件だけ外れる(一致の意図が読めない)
一覧検索やフィルタは「大小無視」なのか「完全一致」なのかが仕様になることが多い。
既定比較のままだと、仕様がコードに残らず、後から判断が面倒になる。
// 仕様としては大小無視のつもりでも、既定では区別する
if (name.Contains(keyword))
{
}
意図を引数で残すと、判断が短くなる。
if (name.Contains(keyword, StringComparison.OrdinalIgnoreCase))
{
}
(.NET Framework 4.8) Contains(value, StringComparison) が無い場合は IndexOf(. , StringComparison) >= 0 へ寄せる。
if (name.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0)
{
}
採用判断基準(迷いどころを潰す)
- Ordinal(言語解釈なし)
- 識別子、キー、コード値、プロトコル上のトークン、ファイルパス、設定キー
- 目的: 「同じバイト列か(大小無視するか)」だけを見たい
- CurrentCulture(言語解釈あり)
- UIに出る文言、ユーザー入力を言語の規則として扱う要件がある比較
- 目的: 「その文化圏の読み方の規則」を反映したい
「どちらでも動く」状態が一番つらい。揺れる余地が残るため、後から迷いが出る。
最短の実装パターン
1) nullでも崩れない形で、規則を明記する
// null安全 + 比較規則が見える
if (string.Equals(a, b, StringComparison.Ordinal)) { }
// 大小文字を無視した識別子比較
if (string.Equals(idA, idB, StringComparison.OrdinalIgnoreCase)) { }
a.Equals(b) は a が null だと落ちる。
比較は string.Equals(a, b, ...) に寄せると形が揃う。
2) 前方一致/末尾一致/検索も、規則を明記する
if (path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { }
if (name.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) { }
var idx = text.IndexOf("bin", StringComparison.OrdinalIgnoreCase);
IndexOf はoverloadで比較規則を指定できるため、引数で意図を残すのが無難。
3) コレクションのキー比較はStringComparerで揃える
var dict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
dict["abc"] = 1;
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
set.Add("ABC");
比較規則をコンテナ側に持たせると、呼び出し側の比較漏れが減る。
.NET Framework 4.8: 併記(差分が有意な箇所だけ)
StringComparisonは .NET Framework 4.8 にも存在する(列挙体として使える)。
差分は「メソッド側がStringComparisonを受け取るoverloadが揃っていない」点にある。
例として string.Contains(string, StringComparison) は .NET Framework 4.8 には無いので、IndexOf で代替する。
bool ContainsOrdinalIgnoreCase(string source, string value)
{
if (source is null || value is null) return false;
return source.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;
}
判例(OK/NG)
| 観点 | OK例 | NG例 | 理由(困る点) | レビューで見る所 |
|---|---|---|---|---|
| 一致判定 | string.Equals(a, b, StringComparison.Ordinal) |
a == b / a.Equals(b)
|
比較規則が読み取りにくい/統一されにくい |
Equals のoverload使用有無 |
| 大小無視 | string.Equals(a, b, StringComparison.OrdinalIgnoreCase) |
a.ToLower() == b.ToLower() |
余計な割り当て、Culture混入の余地 |
ToLower/ToUpper の比較が残っていないか |
| 前方一致 | text.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) |
text.StartsWith(prefix) |
既定の比較が混ざり、意図が見えない |
StartsWith に基準指定があるか |
| 部分一致 | text.Contains(x, StringComparison.OrdinalIgnoreCase) |
text.Contains(x) |
大小やCultureの意図が読めない |
Contains のoverload/Framework差分 |
| (.NET Framework 4.8) 部分一致 | text.IndexOf(x, StringComparison.OrdinalIgnoreCase) >= 0 |
text.Contains(x) |
Contains(. , StringComparison) が無い |
IndexOf への寄せがあるか |
| キー比較 | new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase) |
new Dictionary<string, T>() |
追加/取得で大小が混ざると取れない | 生成時にcomparer指定しているか |
| 識別子用途 | Ordinal/OrdinalIgnoreCase |
CurrentCulture |
用途と規則がズレる | その比較は「識別子」か「表示」か |
レビュー観点
| 観点 | ありがちな見落とし | 困り方 | 指摘コメント例(直球禁止) |
|---|---|---|---|
| 既定比較の混在 | overloadなしの StartsWith/IndexOf が混ざる |
意図の説明が難しく、確認が長引く | 「比較規則がコード上に残らないので、StringComparisonを渡す形だと確認が速い」 |
| 目的の取り違え | 識別子比較にCulture系が混ざる | 端末差が出る余地が残る | 「この比較は言語の意味が要るか。識別子ならOrdinal系が意図に合う」 |
| null経路 |
a.Equals(.) が残る |
null混入で例外になり、原因が散る | 「nullの経路があるならstring.Equals(a, b, .)へ寄せると形が揃う」 |
| コレクションのキー | Dictionary/HashSetの comparer 未指定 | 追加と取得で大小が揺れる | 「キー比較はコンテナ生成時にStringComparerを渡すと漏れが減る」 |
| 変換比較 |
ToLower/ToUpper で比較 |
余計な割り当て、意図が曖昧 | 「大小無視はStringComparisonで表せるので、基準を引数に残す形が読みやすい」 |
禁書庫A: 逆引き(症状→原因→対策)
| 症状 | ありがちな原因 | 切り分け(見る場所) | 最短の対処 | 再発防止(ルール化) |
|---|---|---|---|---|
| 検索が当たらない | 大小文字差、既定比較の混在 |
Contains/StartsWith/IndexOf を列挙し、規則指定の有無を見る |
StringComparison.OrdinalIgnoreCase を明示する |
比較APIは StringComparison を渡す形に寄せる |
| 端末によって一致が揺れる | Cultureの影響が入る比較が混ざる |
CurrentCulture が関わる比較の有無を見る |
識別子用途は Ordinal 系へ寄せる | 「識別子はOrdinal」「表示はCulture」を選定基準として残す |
| キーで取り出せない | Dictionary/HashSetの比較規則が未指定 | コレクション生成箇所を見る |
StringComparer.OrdinalIgnoreCase を渡す |
コレクション生成時に comparer を指定する |
| nullで落ちる |
a.Equals(.) を使う |
例外スタックから比較箇所を見る |
string.Equals(a, b, .) に置換する |
比較は string.Equals へ寄せる |
| 変換で濁っている |
ToLower で比較している |
変換比較をgrepする |
StringComparison 指定へ戻す |
大小無視は変換ではなく比較規則で表す |
禁書庫B: チートシート(決め打ちで読む)
- 識別子/キー/コード値/パス:
Ordinal/OrdinalIgnoreCase - 表示文言の比較: 要件がある場合のみ
CurrentCultureを使う - nullが混ざり得る比較:
string.Equals(a, b, .) - 部分一致:
Contains(. StringComparison.XXX)(.NET Framework 4.8はIndexOf(. StringComparison.XXX) >= 0) - コレクションのキー:
StringComparer.XXXを生成時に渡す