.NET での文字列の比較に関するベスト プラクティス

.NET には、ローカライズされたアプリケーションやグローバル化されたアプリケーションを開発するための広範なサポートが用意されており、文字列の並べ替えや表示などの一般的な操作を実行するときに、現在のカルチャの規則や特定のカルチャの規則を簡単に適用できるようになっています。 しかし、文字列の並べ替えや比較の操作は、必ずしもカルチャに依存するとは限りません。 たとえば、アプリケーションが内部で使用する文字列は、通常、すべてのカルチャで同じように処理される必要があります。 XML タグ、HTML タグ、ユーザー名、ファイル パス、システム オブジェクトの名前などのカルチャに依存しない文字列データがカルチャに依存するかのように解釈されると、アプリケーション コードで軽度のバグが発生したり、パフォーマンスが低下したり、場合によってはセキュリティの問題を引き起こしたりする可能性があります。

ここでは、.NET の文字列の並べ替え、比較、および大文字と小文字の区別のメソッドについて検討し、適切な文字列処理メソッドを選択するための推奨事項と、文字列処理メソッドに関する追加情報を紹介します。

文字列の使用に関する推奨事項

.NET による開発で文字列を比較するときは、以下の推奨事項に従います。

ヒント

さまざまな文字列関連のメソッドで比較が実行されます。 たとえば、String.EqualsString.CompareString.IndexOfString.StartsWith などがあります。

文字列を比較するときに避ける必要があることを次に示します。

  • 文字列操作に対して文字列比較の規則を明示的または暗黙的に指定しないオーバーロードは使用しないようにします。
  • ほとんどの場合、StringComparison.InvariantCulture に基づく文字列操作は使用しません。 数少ない例外の 1 つは、言語的な意味を持つが、カルチャには依存しないデータを永続化する場合です。
  • 2 つの文字列が等価かどうかを確認する場合に、String.Compare または CompareTo メソッドのオーバーロードで戻り値が 0 かどうかをテストする方法は使用しないでください。

文字列比較の明示的な指定

.NET の文字列操作メソッドは、ほとんどがオーバーロードされています。 通常は、既定の設定をそのまま使用する 1 つまたは複数のオーバーロードと、既定の設定を使用せずに文字列の比較または操作の正確な方法を定義するその他のオーバーロードがあります。 既定に依存しないメソッドには、ほとんどの場合、StringComparison型のパラメーターが含まれています。これは、カルチャおよび大文字と小文字の区別によって文字列比較の規則を明示的に指定する列挙型です。 StringComparison 列挙型のメンバーを次の表に示します。

StringComparison のメンバー 説明
CurrentCulture 現在のカルチャを使用して、大文字と小文字を区別する比較を実行します。
CurrentCultureIgnoreCase 現在のカルチャを使用して、大文字と小文字を区別しない比較を実行します。
InvariantCulture インバリアント カルチャを使用して、大文字と小文字を区別する比較を実行します。
InvariantCultureIgnoreCase インバリアント カルチャを使用して、大文字と小文字を区別しない比較を実行します。
Ordinal 序数に基づく比較を実行します。
OrdinalIgnoreCase 大文字と小文字を区別しない、序数に基づく比較を実行します。

たとえば、文字または文字列に一致する String オブジェクト内の部分文字列のインデックスを返す IndexOf メソッドには、次の 9 つのオーバーロードがあります。

次のような理由から、既定値を使用しないオーバーロードを選択することをお勧めします。

  • 既定のパラメーターを持つオーバーロードには、序数に基づく比較を実行するもの (文字列インスタンスで Char を検索するもの) と、カルチャに依存するもの (文字列インスタンスで文字列を検索するもの) があります。 どのメソッドがどの既定値を使用するのかを覚えておくのは難しく、オーバーロードを混同しやすくなります。

  • メソッド呼び出しで既定値に依存するコードは、意図が不明確になります。 既定値に依存する次の例では、2 つの文字列の序数または言語に基づく比較のどちらを開発者が実際に意図したのか、または、url.Scheme と "http" の大文字と小文字の違いにより等価性のテストで false が返されるのかどうかを知るのは困難です。

    Uri url = new("https://learn.microsoft.com/");
    
    // Incorrect
    if (string.Equals(url.Scheme, "https"))
    {
        // ...Code to handle HTTPS protocol.
    }
    
    Dim url As New Uri("https://learn.microsoft.com/")
    
    ' Incorrect
    If String.Equals(url.Scheme, "https") Then
        ' ...Code to handle HTTPS protocol.
    End If
    

一般的に、コードの意図が明確になるため、既定値に依存しないメソッドを呼び出すことをお勧めします。 その結果、コードが読みやすくなるため、デバッグや保守も容易になります。 次の例では、前の例で発生した問題に対応します。 序数比較を使用することと、大文字と小文字の違いを無視することを指定します。

Uri url = new("https://learn.microsoft.com/");

// Correct
if (string.Equals(url.Scheme, "https", StringComparison.OrdinalIgnoreCase))
{
    // ...Code to handle HTTPS protocol.
}
Dim url As New Uri("https://learn.microsoft.com/")

' Incorrect
If String.Equals(url.Scheme, "https", StringComparison.OrdinalIgnoreCase) Then
    ' ...Code to handle HTTPS protocol.
End If

文字列比較の詳細

文字列比較は、多くの文字列関連操作 (特に並べ替えおよび等価性テスト) の中核です。 文字列は、決まった順序で並べられています。たとえば、文字列の並べ替え済みリスト上で "my" が "string" の前にある場合、比較では必ず "my" が "string" 以下になります。 また、比較は等価性を暗黙的に定義します。 比較演算では、等価と見なされた文字列に対して 0 が返されます。 これは、どちらの文字列ももう一方の文字列より小さくないという意味に解釈するとわかりやすくなります。 文字列に関係する、意味のある操作のほとんどには、他の文字列との比較か、正しく定義された並べ替え操作の実行のいずれかまたは両方の処理が含まれています。

注意

Windows オペレーティング システムの並べ替え操作と比較操作で使用される文字の重みに関する情報を含む一連のテキスト ファイルである並べ替え重みテーブル と、Linux と macOS 用の並べ替え重みテーブルの最新バージョンである デフォルト Unicode 照合基本テーブルをダウンロードできます。 Linux と macOS での並べ替え重みのテーブルの特定のバージョンは、システムにインストールされている International Components for Unicode ライブラリのバージョンによって異なります。 実装される ICU のバージョンと Unicode のバージョンに関する情報は、ICU のダウンロードに関する記事を参照してください。

ただし、2 つの文字列の等値または並べ替え順序を評価しても、1 つの正しい結果は得られません。結果は、文字列の比較に使用される条件によって異なります。 特に、序数に基づく文字列比較や、現在のカルチャまたはインバリアント カルチャ (英語をベースとする、ロケールに依存しないカルチャ) の大文字と小文字の規則や並べ替えの規則に基づく文字列比較では、さまざまな結果が返される可能性があります。

さらに、文字列比較を、異なるバージョンの .NET を使用したり、異なるオペレーティング システムまたはバージョンが異なるオペレーティング システム上の .NET で実行したりすると、異なる結果が返る可能性があります。 詳細については、「Strings and The Unicode Standard」(文字列と Unicode 標準) を参照してください。

現在のカルチャを使用する文字列比較

文字列を比較するときの基準として現在のカルチャの規則が使用される場合があります。 現在のカルチャに基づく比較では、スレッドの現在のカルチャ (ロケール) が使用されます。 ユーザーがカルチャを設定していない場合は、オペレーティング システムの設定が既定値になります。 言語的な意味を持つデータや、カルチャに依存したユーザー操作を反映するデータに対しては、常に現在のカルチャに基づく比較を使用する必要があります。

しかし、.NET の比較や大文字と小文字の区別の動作は、カルチャによって変わります。 たとえば、開発されたコンピューターとは異なるカルチャのコンピューターでアプリケーションが実行された場合や、実行中のスレッドのカルチャが変更された場合などに、この変化が生じます。 これは意図的な動作ですが、多くの開発者にはまだあまり知られていません。 次の例は、英語 (米国) ("en-US") とスウェーデン語 ("sv-SE") のカルチャの並べ替え順序の違いを示しています。 並べ替えられた文字列配列で、"ångström"、"Windows"、および "Visual Studio" の位置が違っていることに注目してください。

using System.Globalization;

// Words to sort
string[] values= { "able", "ångström", "apple", "Æble",
                    "Windows", "Visual Studio" };

// Current culture
Array.Sort(values);
DisplayArray(values);

// Change culture to Swedish (Sweden)
string originalCulture = CultureInfo.CurrentCulture.Name;
Thread.CurrentThread.CurrentCulture = new CultureInfo("sv-SE");
Array.Sort(values);
DisplayArray(values);

// Restore the original culture
Thread.CurrentThread.CurrentCulture = new CultureInfo(originalCulture);

static void DisplayArray(string[] values)
{
    Console.WriteLine($"Sorting using the {CultureInfo.CurrentCulture.Name} culture:");
    
    foreach (string value in values)
        Console.WriteLine($"   {value}");

    Console.WriteLine();
}

// The example displays the following output:
//     Sorting using the en-US culture:
//        able
//        Æble
//        ångström
//        apple
//        Visual Studio
//        Windows
//
//     Sorting using the sv-SE culture:
//        able
//        apple
//        Visual Studio
//        Windows
//        ångström
//        Æble
Imports System.Globalization
Imports System.Threading

Module Program
    Sub Main()
        ' Words to sort
        Dim values As String() = {"able", "ångström", "apple", "Æble",
                                  "Windows", "Visual Studio"}

        ' Current culture
        Array.Sort(values)
        DisplayArray(values)

        ' Change culture to Swedish (Sweden)
        Dim originalCulture As String = CultureInfo.CurrentCulture.Name
        Thread.CurrentThread.CurrentCulture = New CultureInfo("sv-SE")
        Array.Sort(values)
        DisplayArray(values)

        ' Restore the original culture
        Thread.CurrentThread.CurrentCulture = New CultureInfo(originalCulture)
    End Sub

    Sub DisplayArray(values As String())
        Console.WriteLine($"Sorting using the {CultureInfo.CurrentCulture.Name} culture:")

        For Each value As String In values
            Console.WriteLine($"   {value}")
        Next

        Console.WriteLine()
    End Sub
End Module

' The example displays the following output:
'     Sorting using the en-US culture:
'        able
'        Æble
'        ångström
'        apple
'        Visual Studio
'        Windows
'
'     Sorting using the sv-SE culture:
'        able
'        apple
'        Visual Studio
'        Windows
'        ångström
'        Æble

現在のカルチャを使用する、大文字と小文字を区別しない比較は、スレッドの現在のカルチャの大文字と小文字の区別の規則が無視される以外は、カルチャに依存した比較と同じです。 この動作も、並べ替え順序に影響する場合があります。

現在のカルチャのセマンティクスを使用する比較は、次のメソッドで既定で使用されます。

どのような場合でも、 StringComparison パラメーターを持つオーバーロードを呼び出して、メソッド呼び出しの意図を明確にすることをお勧めします。

非言語的な文字列データが言語的に解釈されたり、特定のカルチャの文字列データが別のカルチャの規則で解釈されたりすると、軽度のバグやあまり軽度でないバグが発生する可能性があります。 その典型的な例が、トルコ語の I の問題です。

英語 (米国) を含むほぼすべてのラテン アルファベットでは、文字 "i" (\u0069) は "I" (\u0049) の小文字版です。 この大文字と小文字の規則は、このようなカルチャでプログラミングを行う人にとってはすぐに当たり前のことになります。 しかし、トルコ語 ("tr-TR") のアルファベットには、"i" の大文字版である "ドット付きの I" ("İ" (\u0130)) があります。 大文字にすると "I" になる小文字の "ドットなしの i" ("ı" (\u0131)) もあります。 この動作は、アゼルバイジャン語 ("az") のカルチャでも発生します。

したがって、"i" を大文字にしたり "I" を小文字にしたりすることに対する仮定は、すべてのカルチャで有効なわけではありません。 文字列比較ルーチンの既定のオーバーロードを使用すると、カルチャ間の差異の影響を受けることになります。 また、非言語的なデータを比較する場合も、既定のオーバーロードを使用すると望ましくない結果が返される可能性があります。たとえば次の例では、文字列 "bill" と "BILL" の大文字と小文字を区別しない比較を実行しています。

using System.Globalization;

string name = "Bill";

Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
Console.WriteLine($"Culture = {Thread.CurrentThread.CurrentCulture.DisplayName}");
Console.WriteLine($"   Is 'Bill' the same as 'BILL'? {name.Equals("BILL", StringComparison.OrdinalIgnoreCase)}");
Console.WriteLine($"   Does 'Bill' start with 'BILL'? {name.StartsWith("BILL", true, null)}");
Console.WriteLine();

Thread.CurrentThread.CurrentCulture = new CultureInfo("tr-TR");
Console.WriteLine($"Culture = {Thread.CurrentThread.CurrentCulture.DisplayName}");
Console.WriteLine($"   Is 'Bill' the same as 'BILL'? {name.Equals("BILL", StringComparison.OrdinalIgnoreCase)}");
Console.WriteLine($"   Does 'Bill' start with 'BILL'? {name.StartsWith("BILL", true, null)}");

//' The example displays the following output:
//'
//'     Culture = English (United States)
//'        Is 'Bill' the same as 'BILL'? True
//'        Does 'Bill' start with 'BILL'? True
//'     
//'     Culture = Turkish (Türkiye)
//'        Is 'Bill' the same as 'BILL'? True
//'        Does 'Bill' start with 'BILL'? False
Imports System.Globalization
Imports System.Threading

Module Program
    Sub Main()
        Dim name As String = "Bill"

        Thread.CurrentThread.CurrentCulture = New CultureInfo("en-US")
        Console.WriteLine($"Culture = {Thread.CurrentThread.CurrentCulture.DisplayName}")
        Console.WriteLine($"   Is 'Bill' the same as 'BILL'? {name.Equals("BILL", StringComparison.OrdinalIgnoreCase)}")
        Console.WriteLine($"   Does 'Bill' start with 'BILL'? {name.StartsWith("BILL", True, Nothing)}")
        Console.WriteLine()

        Thread.CurrentThread.CurrentCulture = New CultureInfo("tr-TR")
        Console.WriteLine($"Culture = {Thread.CurrentThread.CurrentCulture.DisplayName}")
        Console.WriteLine($"   Is 'Bill' the same as 'BILL'? {name.Equals("BILL", StringComparison.OrdinalIgnoreCase)}")
        Console.WriteLine($"   Does 'Bill' start with 'BILL'? {name.StartsWith("BILL", True, Nothing)}")
    End Sub

End Module

' The example displays the following output:
'
'     Culture = English (United States)
'        Is 'Bill' the same as 'BILL'? True
'        Does 'Bill' start with 'BILL'? True
'     
'     Culture = Turkish (Türkiye)
'        Is 'Bill' the same as 'BILL'? True
'        Does 'Bill' start with 'BILL'? False

この比較は、セキュリティが重要となる状況でカルチャが不注意に使用されると、次の例のような重大な問題を引き起こす可能性があります。 IsFileURI("file:") などのメソッド呼び出しは、現在のカルチャが英語 (米国) の場合は true を返しますが、現在のカルチャがトルコ語の場合は false を返します。 したがって、"FILE:" で始まる URI へのアクセスを大文字と小文字の区別なくブロックするセキュリティ対策は、トルコ語のシステムでは攻略される可能性があります。

public static bool IsFileURI(string path) =>
    path.StartsWith("FILE:", true, null);
Public Shared Function IsFileURI(path As String) As Boolean
    Return path.StartsWith("FILE:", True, Nothing)
End Function

この例の "file:" は、カルチャに依存しない非言語的な識別子として解釈されるものなので、コードを次のように書き換える必要があります。

public static bool IsFileURI(string path) =>
    path.StartsWith("FILE:", StringComparison.OrdinalIgnoreCase);
Public Shared Function IsFileURI(path As String) As Boolean
    Return path.StartsWith("FILE:", StringComparison.OrdinalIgnoreCase)
End Function

序数に基づく文字列操作

メソッド呼び出しで StringComparison.Ordinal 値または StringComparison.OrdinalIgnoreCase 値を指定すると、非言語的な比較が行われ、自然言語の特性は無視されます。 これらの StringComparison 値を使用して呼び出されたメソッドでは、文字列操作の判断が、大文字と小文字の指定、またはカルチャでパラメーター化される同等の表ではなく、単純なバイト比較に基づいて行われます。 これにより、ほとんどの場合に文字列が意図されたとおりに解釈され、コードの実行速度と信頼性も向上します。

序数に基づく比較とは、各文字列の各バイトが言語的に解釈されずに比較される文字列比較です。たとえば、"windows" と "Windows" は一致しません。 これは、実質的には C ランタイムの strcmp 関数の呼び出しです。 文字列が厳密に一致する必要がある状況や、慎重な照合ポリシーが求められる状況では、この比較を使用します。 また、序数に基づく比較は最も高速な比較演算でもあります。これは、結果を判定するときに言語の規則が適用されないためです。

.NET の文字列には、埋め込み null 文字 (およびその他の非印刷文字) を含めることができます。 序数に基づく比較とカルチャに依存した比較 (インバリアント カルチャを使用する比較を含む) の最も明白な違いの 1 つは、文字列に埋め込まれた null 文字の処理に関連しています。 これらの文字は、String.Compare メソッドや String.Equals メソッドを使用して、カルチャに依存した比較 (インバリアント カルチャを使用する比較を含む) を実行する場合には無視されます。 その結果、null 文字が埋め込まれた文字列と埋め込まれていない文字列を等価と見なすことができます。 埋め込まれた非印刷文字は、String.StartsWith などの文字列比較メソッドの目的でスキップされる場合があります。

重要

埋め込まれた null 文字は、文字列比較メソッドでは無視されますが、文字列検索メソッド (String.ContainsString.EndsWithString.IndexOfString.LastIndexOfString.StartsWith など) では無視されません。

次の例では、文字列 "Aa" と、"A" と "a" の間にいくつかの null 文字が埋め込まれた類似の文字列とのカルチャに依存した比較を実行して、2 つの文字列が等価と見なされることを示しています。

string str1 = "Aa";
string str2 = "A" + new string('\u0000', 3) + "a";

Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo("en-us");

Console.WriteLine($"Comparing '{str1}' ({ShowBytes(str1)}) and '{str2}' ({ShowBytes(str2)}):");
Console.WriteLine("   With String.Compare:");
Console.WriteLine($"      Current Culture: {string.Compare(str1, str2, StringComparison.CurrentCulture)}");
Console.WriteLine($"      Invariant Culture: {string.Compare(str1, str2, StringComparison.InvariantCulture)}");
Console.WriteLine("   With String.Equals:");
Console.WriteLine($"      Current Culture: {string.Equals(str1, str2, StringComparison.CurrentCulture)}");
Console.WriteLine($"      Invariant Culture: {string.Equals(str1, str2, StringComparison.InvariantCulture)}");

string ShowBytes(string value)
{
   string hexString = string.Empty;
   for (int index = 0; index < value.Length; index++)
   {
      string result = Convert.ToInt32(value[index]).ToString("X4");
      result = string.Concat(" ", result.Substring(0,2), " ", result.Substring(2, 2));
      hexString += result;
   }
   return hexString.Trim();
}

// The example displays the following output:
//     Comparing 'Aa' (00 41 00 61) and 'Aa' (00 41 00 00 00 00 00 00 00 61):
//        With String.Compare:
//           Current Culture: 0
//           Invariant Culture: 0
//        With String.Equals:
//           Current Culture: True
//           Invariant Culture: True

Module Program
    Sub Main()
        Dim str1 As String = "Aa"
        Dim str2 As String = "A" & New String(Convert.ToChar(0), 3) & "a"

        Console.WriteLine($"Comparing '{str1}' ({ShowBytes(str1)}) and '{str2}' ({ShowBytes(str2)}):")
        Console.WriteLine("   With String.Compare:")
        Console.WriteLine($"      Current Culture: {String.Compare(str1, str2, StringComparison.CurrentCulture)}")
        Console.WriteLine($"      Invariant Culture: {String.Compare(str1, str2, StringComparison.InvariantCulture)}")
        Console.WriteLine("   With String.Equals:")
        Console.WriteLine($"      Current Culture: {String.Equals(str1, str2, StringComparison.CurrentCulture)}")
        Console.WriteLine($"      Invariant Culture: {String.Equals(str1, str2, StringComparison.InvariantCulture)}")
    End Sub

    Function ShowBytes(str As String) As String
        Dim hexString As String = String.Empty

        For ctr As Integer = 0 To str.Length - 1
            Dim result As String = Convert.ToInt32(str.Chars(ctr)).ToString("X4")
            result = String.Concat(" ", result.Substring(0, 2), " ", result.Substring(2, 2))
            hexString &= result
        Next

        Return hexString.Trim()
    End Function

    ' The example displays the following output:
    '     Comparing 'Aa' (00 41 00 61) and 'Aa' (00 41 00 00 00 00 00 00 00 61):
    '        With String.Compare:
    '           Current Culture: 0
    '           Invariant Culture: 0
    '        With String.Equals:
    '           Current Culture: True
    '           Invariant Culture: True
End Module

一方、次の例のように序数に基づく比較を使用すると、これらの文字列は等価とは見なされません。

string str1 = "Aa";
string str2 = "A" + new String('\u0000', 3) + "a";

Console.WriteLine($"Comparing '{str1}' ({ShowBytes(str1)}) and '{str2}' ({ShowBytes(str2)}):");
Console.WriteLine("   With String.Compare:");
Console.WriteLine($"      Ordinal: {string.Compare(str1, str2, StringComparison.Ordinal)}");
Console.WriteLine("   With String.Equals:");
Console.WriteLine($"      Ordinal: {string.Equals(str1, str2, StringComparison.Ordinal)}");

string ShowBytes(string str)
{
    string hexString = string.Empty;
    for (int ctr = 0; ctr < str.Length; ctr++)
    {
        string result = Convert.ToInt32(str[ctr]).ToString("X4");
        result = " " + result.Substring(0, 2) + " " + result.Substring(2, 2);
        hexString += result;
    }
    return hexString.Trim();
}

// The example displays the following output:
//    Comparing 'Aa' (00 41 00 61) and 'A   a' (00 41 00 00 00 00 00 00 00 61):
//       With String.Compare:
//          Ordinal: 97
//       With String.Equals:
//          Ordinal: False
Module Program
    Sub Main()
        Dim str1 As String = "Aa"
        Dim str2 As String = "A" & New String(Convert.ToChar(0), 3) & "a"

        Console.WriteLine($"Comparing '{str1}' ({ShowBytes(str1)}) and '{str2}' ({ShowBytes(str2)}):")
        Console.WriteLine("   With String.Compare:")
        Console.WriteLine($"      Ordinal: {String.Compare(str1, str2, StringComparison.Ordinal)}")
        Console.WriteLine("   With String.Equals:")
        Console.WriteLine($"      Ordinal: {String.Equals(str1, str2, StringComparison.Ordinal)}")
    End Sub

    Function ShowBytes(str As String) As String
        Dim hexString As String = String.Empty

        For ctr As Integer = 0 To str.Length - 1
            Dim result As String = Convert.ToInt32(str.Chars(ctr)).ToString("X4")
            result = String.Concat(" ", result.Substring(0, 2), " ", result.Substring(2, 2))
            hexString &= result
        Next

        Return hexString.Trim()
    End Function

    ' The example displays the following output:
    '    Comparing 'Aa' (00 41 00 61) and 'A   a' (00 41 00 00 00 00 00 00 00 61):
    '       With String.Compare:
    '          Ordinal: 97
    '       With String.Equals:
    '          Ordinal: False
End Module

その次に慎重な方法は、大文字と小文字を区別しない序数に基づく比較です。 この比較では、大文字と小文字の区別のほとんどが無視されます (たとえば、"windows" と "Windows" は一致します)。 ASCII 文字を操作する場合、このポリシーは StringComparison.Ordinal と同等ですが、通常の ASCII の大文字と小文字の区別が無視されます。 したがって、[A, Z] (\u0041-\u005A) の任意の文字が [a,z] (\u0061-\007A) の対応する文字と一致します。 ASCII の範囲外の大文字と小文字の区別には、インバリアント カルチャのテーブルが使用されます。 次に例を示します。

string.Compare(strA, strB, StringComparison.OrdinalIgnoreCase);
String.Compare(strA, strB, StringComparison.OrdinalIgnoreCase)

この比較は、次の比較と同等です (ただし、より高速です)。

string.Compare(strA.ToUpperInvariant(), strB.ToUpperInvariant(), StringComparison.Ordinal);
String.Compare(strA.ToUpperInvariant(), strB.ToUpperInvariant(), StringComparison.Ordinal)

とはいえ、これらの比較はどちらも非常に高速です。

StringComparison.OrdinalStringComparison.OrdinalIgnoreCase は、どちらもバイナリ値を直接使用するため、照合に最適です。 比較の設定が不明な場合は、この 2 つのいずれかの値を使用してください。 ただし、これらの値を使用するとバイトごとの比較が行われるため、言語的な順序 (英語の辞書のような順序) ではなくバイナリの順序で並べ替えられます。 したがって、結果をユーザーに表示すると、ほとんどの場合不自然に見えます。

序数に基づくセマンティクスは、StringComparison 引数を含まない String.Equals のオーバーロード (等価演算子を含む) の既定です。 どのような場合でも、 StringComparison パラメーターを持つオーバーロードを呼び出すことをお勧めします。

インバリアント カルチャを使用する文字列操作

インバリアント カルチャを使用する比較では、静的 CultureInfo.InvariantCulture プロパティから返される CompareInfo プロパティが使用されます。 この動作は、すべてのシステムで同じです。範囲外の文字は、等価のインバリアント文字と見なされる文字に変換されます。 このポリシーは、同じ文字列動作のセットを複数のカルチャにわたって保持する場合に便利ですが、予期しない結果になることもよくあります。

インバリアント カルチャを使用する、大文字と小文字を区別しない比較でも、静的 CultureInfo.InvariantCulture プロパティから返される静的 CompareInfo プロパティが比較情報として使用されます。 変換後の文字の大文字と小文字の違いは無視されます。

StringComparison.InvariantCulture を使用する比較と StringComparison.Ordinal を使用する比較は、ASCII 文字列に対して同じように動作します。 ただし、StringComparison.InvariantCulture では言語的な判断が下されるため、バイト セットとして解釈する必要がある文字列に対しては不適切になることがあります。 CultureInfo.InvariantCulture.CompareInfo オブジェクトのために Compare メソッドで特定の文字のセットが等価と解釈されることもあります。 たとえば、次の例が等価になるのは、インバリアント カルチャでは妥当です。

InvariantCulture: a + ̊ = å

LATIN SMALL LETTER A 文字 "a" (\u0061) は、COMBINING RING ABOVE 文字 "+ " ̊" (\u030a) の横にある場合、LATIN SMALL LETTER A WITH RING ABOVE 文字 "å" (\u00e5) として解釈されます。 この動作は、次の例に示すように、序数に基づく比較とは異なります。

string separated = "\u0061\u030a";
string combined = "\u00e5";

Console.WriteLine("Equal sort weight of {0} and {1} using InvariantCulture: {2}",
                  separated, combined,
                  string.Compare(separated, combined, StringComparison.InvariantCulture) == 0);

Console.WriteLine("Equal sort weight of {0} and {1} using Ordinal: {2}",
                  separated, combined,
                  string.Compare(separated, combined, StringComparison.Ordinal) == 0);

// The example displays the following output:
//     Equal sort weight of a° and å using InvariantCulture: True
//     Equal sort weight of a° and å using Ordinal: False
Module Program
    Sub Main()
        Dim separated As String = ChrW(&H61) & ChrW(&H30A)
        Dim combined As String = ChrW(&HE5)

        Console.WriteLine("Equal sort weight of {0} and {1} using InvariantCulture: {2}",
                          separated, combined,
                          String.Compare(separated, combined, StringComparison.InvariantCulture) = 0)

        Console.WriteLine("Equal sort weight of {0} and {1} using Ordinal: {2}",
                          separated, combined,
                          String.Compare(separated, combined, StringComparison.Ordinal) = 0)

        ' The example displays the following output:
        '     Equal sort weight of a° and å using InvariantCulture: True
        '     Equal sort weight of a° and å using Ordinal: False
    End Sub
End Module

ファイル名やクッキーなど、"å" のような組み合わせが出現する可能性がある要素を解釈する場合にも、序数に基づく比較を使用するのが最も明確かつ適切な方法になります。

結局、インバリアント カルチャには、比較に使用すると便利なプロパティがほとんどありません。 インバリアント カルチャを使用すると、言語的な意味を持つ形で比較が行われるため、記号の完全な等価性は保証されません。その一方で、特定のカルチャでの表示にも適していません。 StringComparison.InvariantCulture を比較に使用する数少ない理由の 1 つは、順序付けされたデータを複数のカルチャで同じように表示するために永続化できることです。 たとえば、表示する並べ替え済みの識別子のリストを含む大きなデータ ファイルがアプリケーションに付属している場合に、そのリストにエントリを追加するには、インバリアント スタイルの並べ替えを使用する挿入が必要になります。

メソッド呼び出しに使用する StringComparison メンバーの選択

文字列のセマンティックなコンテキストと StringComparison 列挙型のメンバーとの対応関係の概要を次の表に示します。

データ 動作 対応する System.StringComparison

大文字と小文字が区別される内部識別子。

XML や HTTP などの標準の、大文字と小文字が区別される識別子。

大文字と小文字が区別されるセキュリティ関連の設定。
バイトが正確に一致する非言語的識別子。 Ordinal
大文字と小文字が区別されない内部識別子。

XML や HTTP などの標準の、大文字と小文字が区別されない識別子。

ファイル パス。

レジストリのキーと値。

環境変数。

リソース識別子 (ハンドル名など)。

大文字と小文字が区別されないセキュリティ関連の設定。
大文字と小文字の区別に関係ない非言語的識別子。 OrdinalIgnoreCase
永続化される、言語的な意味を持つデータの一部。

一定の並べ替え順序を必要とする言語的なデータの表示。
カルチャに依存しないが、言語的な意味を持つデータ。 InvariantCulture

\- または -

InvariantCultureIgnoreCase
ユーザーに表示されるデータ。

ほとんどのユーザー入力。
特定の言語の規則を必要とするデータ。 CurrentCulture

\- または -

CurrentCultureIgnoreCase

.NET の一般的な文字列比較メソッド

以降では、文字列比較でよく使用されるメソッドについて説明します。

String.Compare

既定の解釈: StringComparison.CurrentCulture

このメソッドは文字列解釈の中心的な操作となるため、メソッド呼び出しのすべてのインスタンスを調べて、文字列を現在のカルチャに従って解釈するべきか、カルチャから切り離して (記号として) 扱うべきかどうかを確認する必要があります。 ほとんどは後者であるため、その場合は代わりに StringComparison.Ordinal の比較を使用します。

CultureInfo.CompareInfo プロパティから返される System.Globalization.CompareInfo クラスにも、CompareOptions フラグ列挙体でさまざまな照合方法 (序数に基づく、空白文字を無視する、カナ型を無視するなど) を指定できる Compare メソッドが含まれています。

String.CompareTo

既定の解釈: StringComparison.CurrentCulture

このメソッドには、現在、StringComparison 型を指定するオーバーロードがありません。 通常は、このメソッドを推奨される String.Compare(String, String, StringComparison) の形式に変換できます。

このメソッドは、 IComparable インターフェイスと IComparable<T> インターフェイスを実装する型に実装されます。 StringComparison パラメーターのオプションは提供されないため、型を実装すると、多くの場合、ユーザーがコンストラクターで StringComparer を指定できます。 次の例では、クラス コンストラクターに StringComparer パラメーターを含む FileName クラスを定義しています。 この StringComparer オブジェクトは、その後、 FileName.CompareTo メソッドで使用されています。

class FileName : IComparable
{
    private readonly StringComparer _comparer;

    public string Name { get; }

    public FileName(string name, StringComparer? comparer)
    {
        if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));

        Name = name;

        if (comparer != null)
            _comparer = comparer;
        else
            _comparer = StringComparer.OrdinalIgnoreCase;
    }

    public int CompareTo(object? obj)
    {
        if (obj == null) return 1;

        if (obj is not FileName)
            return _comparer.Compare(Name, obj.ToString());
        else
            return _comparer.Compare(Name, ((FileName)obj).Name);
    }
}
Class FileName
    Implements IComparable

    Private ReadOnly _comparer As StringComparer

    Public ReadOnly Property Name As String

    Public Sub New(name As String, comparer As StringComparer)
        If (String.IsNullOrEmpty(name)) Then Throw New ArgumentNullException(NameOf(name))

        Me.Name = name

        If comparer IsNot Nothing Then
            _comparer = comparer
        Else
            _comparer = StringComparer.OrdinalIgnoreCase
        End If
    End Sub

    Public Function CompareTo(obj As Object) As Integer Implements IComparable.CompareTo
        If obj Is Nothing Then Return 1

        If TypeOf obj IsNot FileName Then
            Return _comparer.Compare(Name, obj.ToString())
        Else
            Return _comparer.Compare(Name, DirectCast(obj, FileName).Name)
        End If
    End Function
End Class

String.Equals

既定の解釈: StringComparison.Ordinal

String クラスで等価性テストを実行するには、 Equals メソッド (静的メソッドまたはインスタンス メソッド) のオーバーロードを呼び出すか、静的等値演算子を使用します。 これらのオーバーロードと演算子では、序数に基づく比較が既定で使用されます。 しかし、序数に基づく比較を実行する場合でも、StringComparison 型を明示的に指定するオーバーロードを呼び出すことをお勧めします。これにより、特定の文字列解釈のコードを検索しやすくなります。

String.ToUpper と String.ToLower

既定の解釈: StringComparison.CurrentCulture

String.ToUpper() メソッドと String.ToLower() メソッドを使用する場合は注意が必要です。文字列を強制的に大文字または小文字にすることは、大文字、小文字に関係なく文字列を比較するための小規模な正規化としてよく使用されるためです。 その場合は、大文字と小文字を区別しない比較を使用することを検討してください。

String.ToUpperInvariant メソッドと String.ToLowerInvariant メソッドを使用することもできます。 ToUpperInvariant は、大文字と小文字を正規化するための標準的な方法です。 StringComparison.OrdinalIgnoreCase を使用して行われる比較は、動作の内容を見ると、両方の文字列引数に対して ToUpperInvariant を呼び出し、StringComparison.Ordinal を使用して比較を行うという、2 つの呼び出しの組み合わせです。

特定のカルチャを表す CultureInfo オブジェクトをメソッドに渡して、そのカルチャで大文字および小文字への変換を行うためのオーバーロードもあります。

Char.ToUpper と Char.ToLower

既定の解釈: StringComparison.CurrentCulture

Char.ToUpper(Char)Char.ToLower(Char) メソッドは、前のセクションで説明した String.ToUpper()String.ToLower() メソッドと同様に機能します。

String.StartsWith と String.EndsWith

既定の解釈: StringComparison.CurrentCulture

これらのメソッドは、いずれもカルチャに依存した比較を既定で実行します。 特に非印刷文字は無視される場合があります。

String.IndexOf と String.LastIndexOf

既定の解釈: StringComparison.CurrentCulture

これらのメソッドの既定のオーバーロードは、比較の実行方法が一貫していません。 Char パラメーターを含むすべての String.IndexOf メソッドと String.LastIndexOf メソッドは、序数に基づく比較を実行します。一方、String パラメーターを含む既定の String.IndexOf メソッドと String.LastIndexOf メソッドは、カルチャに依存した比較を実行します。

String.IndexOf(String) メソッドまたは String.LastIndexOf(String) メソッドを呼び出して、現在のインスタンスで検索する文字列を渡す場合は、StringComparison 型を明示的に指定するオーバーロードを呼び出すことをお勧めします。 Char 引数を含むオーバーロードでは、StringComparison 型を指定することはできません。

間接的に文字列比較を実行するメソッド

文字列比較を中心的な操作とする非文字列メソッドの中には、 StringComparer 型を使用するものがあります。 StringComparer クラスには、StringComparer のインスタンスを返す静的プロパティが 6 つ含まれています。これらのインスタンスの StringComparer.Compare メソッドは、次の種類の文字列比較を実行します。

Array.Sort と Array.BinarySearch

既定の解釈: StringComparison.CurrentCulture

データをコレクションに格納したり、永続化されたデータをファイルやデータベースからコレクションに読み取ったりするときに現在のカルチャを切り替えると、コレクション内のインバリアントが無効になる可能性があります。 Array.BinarySearch メソッドでは、配列内で検索する要素が既に並べ替えられていると見なされます。 Array.Sort メソッドは、配列内の文字列要素を並べ替えるために、String.Compare メソッドを呼び出して個々の要素を順序付けます。 配列の並べ替えが行われてから内容の検索が行われるまでの間にカルチャが変更される場合、カルチャに依存した比較子を使用するのは危険です。 たとえば、次のコードでは、Thread.CurrentThread.CurrentCulture プロパティが暗黙的に指定した比較子で格納と取得の操作が行われます。 StoreNames の呼び出しと DoesNameExistの呼び出しの間にカルチャが変更されると (この 2 つのメソッドの呼び出しの間に配列の内容が永続化された場合には特に)、バイナリ サーチが失敗する可能性があります。

// Incorrect
string[] _storedNames;

public void StoreNames(string[] names)
{
    _storedNames = new string[names.Length];

    // Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length);

    Array.Sort(_storedNames); // Line A
}

public bool DoesNameExist(string name) =>
    Array.BinarySearch(_storedNames, name) >= 0; // Line B
' Incorrect
Dim _storedNames As String()

Sub StoreNames(names As String())
    ReDim _storedNames(names.Length - 1)

    ' Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length)

    Array.Sort(_storedNames) ' Line A
End Sub

Function DoesNameExist(name As String) As Boolean
    Return Array.BinarySearch(_storedNames, name) >= 0 ' Line B
End Function

次の例は、推奨されるバリエーションを示しています。ここでは、配列の並べ替えと検索の両方に、同じ序数に基づく (カルチャに依存しない) 比較メソッドが使用されています。 コードの変更は、2 つの例の Line A および Line B というラベルが付いた行に反映されています。

// Correct
string[] _storedNames;

public void StoreNames(string[] names)
{
    _storedNames = new string[names.Length];

    // Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length);

    Array.Sort(_storedNames, StringComparer.Ordinal); // Line A
}

public bool DoesNameExist(string name) =>
    Array.BinarySearch(_storedNames, name, StringComparer.Ordinal) >= 0; // Line B
' Correct
Dim _storedNames As String()

Sub StoreNames(names As String())
    ReDim _storedNames(names.Length - 1)

    ' Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length)

    Array.Sort(_storedNames, StringComparer.Ordinal) ' Line A
End Sub

Function DoesNameExist(name As String) As Boolean
    Return Array.BinarySearch(_storedNames, name, StringComparer.Ordinal) >= 0 ' Line B
End Function

このデータを永続化して別のカルチャのシステムに移動したり、データをユーザーに表示するために並べ替えたりする場合は、StringComparison.InvariantCulture を使用することを検討してください。そうすると、ユーザー出力のために言語的な操作を行っても、カルチャの変更による影響を受けることはありません。 次の例では、前の 2 つの例を変更して、配列の並べ替えと検索にインバリアント カルチャを使用しています。

// Correct
string[] _storedNames;

public void StoreNames(string[] names)
{
    _storedNames = new string[names.Length];

    // Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length);

    Array.Sort(_storedNames, StringComparer.InvariantCulture); // Line A
}

public bool DoesNameExist(string name) =>
    Array.BinarySearch(_storedNames, name, StringComparer.InvariantCulture) >= 0; // Line B
' Correct
Dim _storedNames As String()

Sub StoreNames(names As String())
    ReDim _storedNames(names.Length - 1)

    ' Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length)

    Array.Sort(_storedNames, StringComparer.InvariantCulture) ' Line A
End Sub

Function DoesNameExist(name As String) As Boolean
    Return Array.BinarySearch(_storedNames, name, StringComparer.InvariantCulture) >= 0 ' Line B
End Function

コレクションの例: Hashtable のコンストラクター

文字列の比較方法の影響を受ける操作の 2 例目は文字列のハッシュです。

次の例では、StringComparer.OrdinalIgnoreCase プロパティから返される StringComparer オブジェクトを渡して Hashtable オブジェクトをインスタンス化しています。 StringComparer から派生するクラス StringComparerIEqualityComparer インターフェイスを実装するため、その GetHashCode メソッドを使用して、ハッシュ テーブルの文字列のハッシュ コードを計算しています。

using System.IO;
using System.Collections;

const int InitialCapacity = 100;

Hashtable creationTimeByFile = new(InitialCapacity, StringComparer.OrdinalIgnoreCase);
string directoryToProcess = Directory.GetCurrentDirectory();

// Fill the hash table
PopulateFileTable(directoryToProcess);

// Get some of the files and try to find them with upper cased names
foreach (var file in Directory.GetFiles(directoryToProcess))
    PrintCreationTime(file.ToUpper());


void PopulateFileTable(string directory)
{
    foreach (string file in Directory.GetFiles(directory))
        creationTimeByFile.Add(file, File.GetCreationTime(file));
}

void PrintCreationTime(string targetFile)
{
    object? dt = creationTimeByFile[targetFile];

    if (dt is DateTime value)
        Console.WriteLine($"File {targetFile} was created at time {value}.");
    else
        Console.WriteLine($"File {targetFile} does not exist.");
}
Imports System.IO

Module Program
    Const InitialCapacity As Integer = 100

    Private ReadOnly s_creationTimeByFile As New Hashtable(InitialCapacity, StringComparer.OrdinalIgnoreCase)
    Private ReadOnly s_directoryToProcess As String = Directory.GetCurrentDirectory()

    Sub Main()
        ' Fill the hash table
        PopulateFileTable(s_directoryToProcess)

        ' Get some of the files and try to find them with upper cased names
        For Each File As String In Directory.GetFiles(s_directoryToProcess)
            PrintCreationTime(File.ToUpper())
        Next
    End Sub

    Sub PopulateFileTable(directoryPath As String)
        For Each file As String In Directory.GetFiles(directoryPath)
            s_creationTimeByFile.Add(file, IO.File.GetCreationTime(file))
        Next
    End Sub

    Sub PrintCreationTime(targetFile As String)
        Dim dt As Object = s_creationTimeByFile(targetFile)

        If TypeOf dt Is Date Then
            Console.WriteLine($"File {targetFile} was created at time {DirectCast(dt, Date)}.")
        Else
            Console.WriteLine($"File {targetFile} does not exist.")
        End If
    End Sub
End Module

関連項目