文字列比較の落とし穴:「カルチャ」依存
C#で2つの文字列を比較する際、== 演算子や引数なしの Equals() メソッドを使うのが最も簡単です。
string input = "windows";
string constant = "WINDOWS";
bool isEqual = (input == constant); // false
bool isEqualEquals = input.Equals(constant); // false
これらの比較は、デフォルトで大文字と小文字を厳密に区別します。では、大文字・小文字を区別せずに比較したい場合はどうでしょうか。ToLowerInvariant()などで小文字に統一する方法もありますが、C#にはより適切な「比較ルール」を指定する方法が用意されています。
ここで重要になるのが、「カルチャ(地域設定)」の問題です。
なぜカルチャを意識する必要があるのか?
string.Equals(stringA, stringB, StringComparison.CurrentCultureIgnoreCase) のように、CurrentCulture(現在のカルチャ)を指定すると、プログラムが実行されているOSの地域設定(例: “ja-JP” や “en-US”)に基づいて比較が行われます。
これは、ユーザーに表示する文字列をソート(並べ替え)する際には正しい動作です。
しかし、**内部的なデータ(ID、ファイル名、APIキー、プロトコル名)**を比較する際にカルチャに依存すると、深刻なバグを引き起こす可能性があります。最も有名な例が「トルコ語の i」です。
- 英語(Invariant Culture):
iの大文字はI - トルコ語(
tr-TR):iの大文字はİ(ドット付き)、Iの小文字はı(ドットなし)
もし、プログラムがトルコ語環境で実行された場合、"file" という文字列と "FILE" という文字列を CurrentCultureIgnoreCase で比較すると、i と I のマッピングが異なるため、比較が false になる可能性があります。
解決策:StringComparison の指定
このようなカルチャ依存のバグを避け、予測可能で一貫性のある比較を行うために、string.Equals や string.Compare メソッドには比較ルール(StringComparison 列挙型)を指定するオーバーロードが用意されています。
内部的なデータ比較や、セキュリティに関わる比較で推奨されるのは Ordinal です。
StringComparison.Ordinal (序数比較)
- 動作: 文字列を単純なバイト列(Unicodeの序数値)として比較します。
- カルチャ: 完全に無視します(最速)。
- 大文字/小文字: 区別します。
- 用途: ファイルパス、APIキー、JSONプロパティ名、ミューテックス名など、プログラム内部で扱うデータや、機械が解釈する文字列の厳密な一致判定。
StringComparison.OrdinalIgnoreCase (序数比較・大文字小文字無視)
- 動作: バイト列として比較しますが、大文字と小文字の区別を無視します。
- カルチャ: 完全に無視します(高速)。
- 大文字/小文字: 無視します。
- 用途: ユーザー名(
adminとADMINを同一視)、HTTPヘッダー、環境変数名など、大文字・小文字を区別しないが、カルチャには依存させたくない場合の比較。
コード例:Ordinal と OrdinalIgnoreCase
"WINDOWS" と "windows" という2つの文字列を、カルチャに依存しない方法で比較します。
using System;
using System.Globalization;
public class OrdinalComparisonExample
{
public static void Main()
{
string id1 = "WINDOWS-KEY-001";
string id2 = "windows-key-001";
Console.WriteLine($"文字列A: {id1}");
Console.WriteLine($"文字列B: {id2}");
Console.WriteLine("---");
// --- 1. Ordinal (厳密なバイト比較) ---
// 大文字と小文字は区別されるため、false になる
bool isEqualOrdinal = id1.Equals(id2, StringComparison.Ordinal);
Console.WriteLine($"Ordinal (区別する): {isEqualOrdinal}");
// --- 2. OrdinalIgnoreCase (バイト比較・大文字小文字無視) ---
// カルチャに依存せず、大文字・小文字を無視するため、true になる
// ユーザー名やキーワードの比較に最適
bool isEqualOrdinalIgnoreCase = id1.Equals(id2, StringComparison.OrdinalIgnoreCase);
Console.WriteLine($"OrdinalIgnoreCase (無視する): {isEqualOrdinalIgnoreCase}");
// --- 3. StartsWith や EndsWith でも同様 ---
string prefix = "windows";
// "WINDOWS-KEY..." が "windows" (小文字) で始まるか?
bool startsWithIgnoreCase = id1.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
Console.WriteLine($"StartsWith (無視する): {startsWithIgnoreCase}");
}
}
出力結果:
文字列A: WINDOWS-KEY-001
文字列B: windows-key-001
---
Ordinal (区別する): False
OrdinalIgnoreCase (無視する): True
StartsWith (無視する): True
まとめ
C#で文字列を比較(Equals, Compare, StartsWith, EndsWithなど)する際は、常に比較ルールを意識する必要があります。
- 内部データ、ID、キー、ファイルパスなど:
- 大文字・小文字を区別する場合:
StringComparison.Ordinal - 大文字・小文字を無視する場合:
StringComparison.OrdinalIgnoreCase
- 大文字・小文字を区別する場合:
- ユーザーに表示する文字列の並べ替え:
StringComparison.CurrentCulture
セキュリティや意図しない動作(バグ)を防ぐため、== 演算子や引数なしの Equals() の使用は避け、StringComparison を明示的に指定することが強く推奨されます。
