0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

R03:【掟・判例】文字列比較の掟 (まれに)同じに見えるのに一致しないを終わらせる ― StringComparisonの習得

Last updated at Posted at 2026-01-17

連載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 を生成時に渡す

関連トピック

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?