連載Index(読む順・公開済リンクが最新): S00_門前の誓い_総合Index
ToLower() 比較を string.Equals(..., StringComparison.OrdinalIgnoreCase) に直しても、Contains と Dictionary<string, T> の比較条件は別に残ります。
Equals だけ直しても、大文字小文字を無視したい場所が全部同じ扱いになるわけではありません。Contains は StringComparison、Dictionary<string, T> や HashSet<string> は StringComparer を別に持つためです。
このページでは、.NET 8 で比較条件が残りやすい場所と、StringComparison と StringComparer の分かれ方だけに絞って書きます。.NET Framework 4.8.1 は後半に補足として載せます。
Equals に直しても比較条件が残る場所
Equals へ置き換えても、比較条件が別に残る場所があります。
-
ToLower(ToUpper( -
StartsWith(EndsWith(Contains( -
new Dictionary<stringnew HashSet<string>
先に置き換えの形だけ並べると、こうなります。
| 残りやすい形 | 置き換える形 |
|---|---|
a.ToLower() == b.ToLower() |
string.Equals(a, b, StringComparison.OrdinalIgnoreCase) |
text.StartsWith(prefix) |
text.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) |
text.EndsWith(value) |
text.EndsWith(value, StringComparison.OrdinalIgnoreCase) |
text.Contains(value) |
text.Contains(value, StringComparison.OrdinalIgnoreCase) |
new Dictionary<string, T>() |
new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase) |
new HashSet<string>() |
new HashSet<string>(StringComparer.OrdinalIgnoreCase) |
StringComparison と StringComparer は書く場所が違う
比較条件がどこで決まるかは、2つに分かれます。
| 比較条件を書く場所 | 使うもの | 例 |
|---|---|---|
| メソッドへ渡す | StringComparison |
string.Equals(a, b, StringComparison.OrdinalIgnoreCase) |
| 辞書や集合を作る時に渡す | StringComparer |
new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase) |
Contains と StartsWith はメソッド呼び出し側です。
Dictionary<string, T> と HashSet<string> は、作る時点で比較条件を持ちます。
ここを分けて見ておくと、Equals だけ直して終わったつもりになる戻りを減らせます。
ToLower() / ToUpper() を比較のために使わない
ToLower() を使った比較は、まだ残りやすいです。
var isSameUserId = userIdA.ToLower() == userIdB.ToLower();
この形だと、比較のためだけに文字列を作っています。
しかも、何を無視する比較なのかが式から見えません。
比較条件をそのまま書くと、式だけで意図が読めます。
var isSameUserId = string.Equals(userIdA, userIdB, StringComparison.OrdinalIgnoreCase);
大小文字を区別する完全一致なら、こう書けます。
var isSameCode = string.Equals(left, right, StringComparison.Ordinal);
a.Equals(b) より string.Equals(a, b, ...) の形にしておくと、a が null の場合も扱いやすくなります。
Contains StartsWith EndsWith は比較条件を引数へ出す
次に残りやすいのが、比較条件が見えない呼び出しです。
var hasPrefix = code.StartsWith(prefix);
var hasSuffix = fileName.EndsWith(".csv");
var containsKeyword = text.Contains(keyword);
この形だと、大文字小文字を無視する部分一致なのか、既定比較のままなのかが式だけでは分かりません。
大文字小文字を無視したいなら、引数へ出しておきます。
var hasPrefix = code.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
var hasSuffix = fileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase);
var containsKeyword = text.Contains(keyword, StringComparison.OrdinalIgnoreCase);
識別子、コード値、拡張子のように文字列を値として扱う場面では、OrdinalIgnoreCase から入ると判断がぶれにくくなります。
Dictionary<string, T> HashSet<string> は作る時点で決まる
ここは Equals 側を直した後も残りやすい場所です。
メソッド側だけ変えても、辞書や集合の側が既定のままだと、キー比較は別の条件のままです。
既定のまま作っている例です。
var customerMap = new Dictionary<string, int>();
customerMap["UserId"] = 100;
var exists = customerMap.ContainsKey("userid");
この形だと、"UserId" と "userid" は別のキーです。
大文字小文字を無視したいなら、作る時に StringComparer を渡します。
var customerMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
customerMap["UserId"] = 100;
var exists = customerMap.ContainsKey("userid");
HashSet<string> も同じです。
var codes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
codes.Add("ABC");
var found = codes.Contains("abc");
呼び出し側で毎回 ToLower() や個別比較に寄せると、追加・取得・検索で比較条件がばらけます。作成時に comparer を決めておくと、振る舞いが定義箇所だけで読めます。
Ordinal と CurrentCulture をどこで分けるか
迷いやすいのは、どこで Ordinal 系と CurrentCulture 系を分けるかです。
| 文字列の中身 | 使うもの |
|---|---|
| 識別子、キー、コード値、設定名 |
StringComparison.Ordinal / StringComparison.OrdinalIgnoreCase
|
| 画面文言の検索で、現在の言語設定に合わせたいもの |
StringComparison.CurrentCulture / StringComparison.CurrentCultureIgnoreCase
|
Dictionary<string, T> HashSet<string>
|
StringComparer.Ordinal / StringComparer.OrdinalIgnoreCase
|
識別子とコード値は、人が読む文より値として扱うことが多いため、Ordinal 系から入ると分けやすくなります。
var isSameUserId = string.Equals(userIdA, userIdB, StringComparison.OrdinalIgnoreCase);
var containsTitle = title.Contains(input, StringComparison.CurrentCultureIgnoreCase);
迷った時は、識別子とコード値は Ordinal 系、画面文言の検索だけ CurrentCulture 系で分ければ足ります。
補足: .NET Framework 4.8.1 では Contains の書き方だけ違う
.NET Framework 4.8.1 では、Contains(value, StringComparison.XXX) は使えません。
部分一致は IndexOf で書きます。
var containsKeyword = text.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0;
StartsWith と EndsWith は比較条件を渡せるため、そこは .NET 8 と同じ見方で問題ありません。
置き換え漏れを見つける検索語
既存コードで置き換え漏れを探すなら、次の検索語から入ると見つけやすくなります。
| 検索語 | 確認すること |
|---|---|
ToLower( / ToUpper(
|
比較のためだけに文字列を作っていないか |
Contains( |
大文字小文字を無視したい場所で比較条件を渡しているか |
new Dictionary<string |
StringComparer を渡しているか |
new HashSet<string |
StringComparer を渡しているか |
StartsWith( / EndsWith(
|
比較条件を渡しているか |
Equals( |
string.Equals(a, b, ...) の形で書いているか |
時間がない時は、この順で見ると拾いやすくなります。
-
ToLower(/ToUpper( Contains(new Dictionary<stringnew HashSet<string-
StartsWith(/EndsWith( Equals(
最初の4つまで見れば、主要な置き換え漏れはかなり拾えます。
置き換え後に崩れやすい場所
置き換えた後は、この表だけ見れば比較条件の取りこぼしを確認できます。
| 見る場所 | 確認すること | 残りやすい状態 |
|---|---|---|
ToLower( ToUpper(
|
比較のためだけに使っていないか | 一致判定だけ古いまま |
StartsWith( EndsWith( Contains(
|
比較条件が引数へ出ているか | 部分一致だけ既定条件のまま |
Dictionary<string, T> |
StringComparer を作成時に渡しているか |
追加と取得で条件が分かれる |
HashSet<string> |
StringComparer を作成時に渡しているか |
追加時と検索時で見方が分かれる |
| 画面文言の検索 |
CurrentCulture 系が必要か |
コード値と同じ条件で比較している |
.NET Framework 4.8.1 |
部分一致を IndexOf で書いているか |
Contains のまま残る |
最小コードまとめ
最後に、置き換え後の形だけまとめて置いておきます。
var sameUserId = string.Equals(userIdA, userIdB, StringComparison.OrdinalIgnoreCase);
var hasPrefix = code.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
var hasSuffix = fileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase);
var hasKeyword = text.Contains(keyword, StringComparison.OrdinalIgnoreCase);
var customerMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var codeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
.NET Framework 4.8.1 の部分一致だけは、こちらです。
var hasKeyword = text.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0;
連載Index(読む順・公開済リンクが最新): S00_門前の誓い_総合Index