次の方法で共有


.NET で文字列を比較するためのベスト プラクティス

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

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

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

.NET を使用して開発する場合は、文字列を比較するときに次の推奨事項に従います。

ヒント

さまざまな文字列関連のメソッドが比較を実行します。 例としては、 String.EqualsString.CompareString.IndexOfString.StartsWithなどがあります。

文字列を比較するときは、次のプラクティスを避けてください。

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

ヒント

CA1307CA1309および CA1310 コード分析ルールは、言語比較子が意図せず使用される呼び出しサイトを識別するのに役立ちます。 それらを有効にし、違反をビルド エラーとして表示するには、プロジェクト ファイルで次のプロパティを設定します。

<PropertyGroup>
  <AnalysisMode>All</AnalysisMode>
  <WarningsAsErrors>$(WarningsAsErrors);CA1307;CA1309;CA1310</WarningsAsErrors>
</PropertyGroup>

文字列比較を明示的に指定する

.NET の文字列操作メソッドのほとんどはオーバーロードされています。 通常、1 つ以上のオーバーロードは既定の設定を受け入れますが、他のオーバーロードは既定値を受け入れず、代わりに文字列を比較または操作する正確な方法を定義します。 既定値に依存しないほとんどのメソッドには、 StringComparison型のパラメーターが含まれます。これは、カルチャと大文字と小文字による文字列比較の規則を明示的に指定する列挙体です。 次の表では、 StringComparison 列挙メンバーについて説明します。

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

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

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

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

  • メソッド呼び出しの既定値に依存するコードの意図は明確ではありません。 既定値に依存する次の例では、開発者が実際に2つの文字列の序数比較または言語的比較を意図しているのか、あるいはurl.Schemeと"HTTPS"の大文字と小文字の違いが原因で等価性のテストが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

文字列比較の詳細

文字列比較は、特に並べ替えと等価性のテストなど、文字列関連の多くの操作の中心です。 文字列は、決められた順序で並べ替えられます。文字列の並べ替えられたリストで "string" の前に "my" が表示される場合、"my" は "string" 以下を比較する必要があります。 さらに、比較によって等価性が暗黙的に定義されます。 比較操作は、等しいと見なされる文字列に対して 0 を返します。 適切な解釈は、どちらの文字列も他の文字列よりも小さいという点です。 文字列に関連する最も意味のある操作には、別の文字列との比較と、明確に定義された並べ替え操作の実行のいずれかまたは両方のプロシージャが含まれます。

並べ替えの重みテーブル、Windows オペレーティング システムの並べ替えおよび比較操作で使用される文字の重みに関する情報を含むテキスト ファイルのセット、およびLinuxとmacOS用の既定のUnicode照合順序要素テーブル、つまり並べ替え重みテーブルの最新バージョンをダウンロードできます。 Linux および macOS の並べ替え重みテーブルの特定のバージョンは、システムにインストールされている Unicode ライブラリ用の International Components のバージョンによって異なります。 ICU のバージョンと実装されている Unicode バージョンについては、「 ICU のダウンロード」を参照してください。

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

さらに、異なるバージョンの .NET を使用する文字列比較や、異なるオペレーティング システムまたはオペレーティング システムのバージョンで .NET を使用すると、異なる結果が返される場合があります。 .NET では、サポートされているすべてのプラットフォームで言語的な文字列比較に Unicode (ICU) 用の International Components (International Components) ライブラリを使用します。 詳細については、「 文字列と Unicode 標準および.NET グローバリゼーションと ICU」を参照してください。

現在のカルチャを基準に文字列を比較する

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

しかし、.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 パラメーターを持つオーバーロードを呼び出して、メソッド呼び出しの意図を明確にすることをお勧めします。

非言語的な文字列データが言語的に解釈される場合や、特定のカルチャの文字列データが別のカルチャの規則を使用して解釈される場合は、微妙でさりげないバグが発生する可能性があります。 標準的な例は、Turkish-I の問題です。

米国英語を含むほぼすべてのラテンアルファベットの場合、文字 "i" (\u0069) は文字 "I" (\u0049) の小文字のバージョンです。 この大文字と小文字の規則は、このようなカルチャでプログラミングを行う人にとってはすぐに当たり前のことになります。 しかし、トルコ語 ("tr-TR") のアルファベットには、"i" の大文字版である "ドット付きの I" ("İ" (\u0130)) があります。 トルコ語には、小文字の "i without a dot" 文字 "ı" (\u0131) も含まれています。これは "I" を大文字にします。 この動作は、アゼルバイジャン語 ("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 関数の呼び出しです。 この比較は、文字列を厳密に照合するか、保守的な照合ポリシーを要求する場合に、コンテキストによって指示される場合に使用します。 さらに、序数比較は、結果を決定するときに言語規則を適用しないため、最も高速な比較操作です。

OrdinalIgnoreCase 比較クラスは文字ごとに動作しますが、操作を行う際に大文字と小文字の違いを無視します。 OrdinalIgnoreCase比較子では、文字ペア'd''D'は、文字ペア'á''Á'比較されます。 ただし、アクセントのない文字'a'は、アクセント記号付きの文字'á'等しくないとされます。

この例をいくつか次の表に示します。

文字列 1 文字列 2 Ordinal 比較 OrdinalIgnoreCase 比較
"dog" "dog" 等しい 等しい
"dog" "Dog" 等しくない 等しい
"resume" "résumé" 等しくない 等しくない

Unicode では、文字列に複数の異なるメモリ内表現を含めることもできます。 たとえば、e-acute (é) は、次の 2 つの方法で表すことができます。

  • 1 つのリテラル 'é' 文字 ( '\u00E9'としても書き込まれます)。
  • アクセントのないリテラル'e'文字の後に、結合文字のアクセント修飾子'\u0301'があります。

つまり、次の 4 つの 文字列は、構成部分が異なる場合でも、すべて "résumé"として表示されます。 文字列では、リテラル 'é' 文字またはアクセントのないリテラル 'e' 文字と結合アクセント修飾子 '\u0301' の組み合わせを使用します。

  • "r\u00E9sum\u00E9"
  • "r\u00E9sume\u0301"
  • "re\u0301sum\u00E9"
  • "re\u0301sume\u0301"

序数比較子では、これらの文字列のいずれも互いに等しいと比較しません。 これは、すべてが異なる基になる文字シーケンスを含んでいるためです。画面にレンダリングされた場合でも、それらはすべて同じように見えます。

string.IndexOf(..., StringComparison.Ordinal)操作を実行すると、ランタイムは完全に一致する部分文字列を検索します。 結果は次のとおりです。

Console.WriteLine("resume".IndexOf('e', StringComparison.Ordinal)); // "resume": prints '1'
Console.WriteLine("r\u00E9sum\u00E9".IndexOf('e', StringComparison.Ordinal)); // "résumé": prints '-1'
Console.WriteLine("r\u00E9sume\u0301".IndexOf('e', StringComparison.Ordinal)); // "résumé": prints '5'
Console.WriteLine("re\u0301sum\u00E9".IndexOf('e', StringComparison.Ordinal)); // "résumé": prints '1'
Console.WriteLine("re\u0301sume\u0301".IndexOf('e', StringComparison.Ordinal)); // "résumé": prints '1'
Console.WriteLine("resume".IndexOf('e', StringComparison.OrdinalIgnoreCase)); // "resume": prints '1'
Console.WriteLine("r\u00E9sum\u00E9".IndexOf('e', StringComparison.OrdinalIgnoreCase)); // "résumé": prints '-1'
Console.WriteLine("r\u00E9sume\u0301".IndexOf('e', StringComparison.OrdinalIgnoreCase)); // "résumé": prints '5'
Console.WriteLine("re\u0301sum\u00E9".IndexOf('e', StringComparison.OrdinalIgnoreCase)); // "résumé": prints '1'
Console.WriteLine("re\u0301sume\u0301".IndexOf('e', StringComparison.OrdinalIgnoreCase)); // "résumé": prints '1'
Sub IndexOfExample()
    Console.WriteLine("resume".IndexOf("e"c, StringComparison.Ordinal)) ' "resume": prints '1'
    Console.WriteLine(("r" & ChrW(&HE9) & "sum" & ChrW(&HE9)).IndexOf("e"c, StringComparison.Ordinal)) ' "résumé": prints '-1'
    Console.WriteLine(("r" & ChrW(&HE9) & "sume" & ChrW(&H301)).IndexOf("e"c, StringComparison.Ordinal)) ' "résumé": prints '5'
    Console.WriteLine(("re" & ChrW(&H301) & "sum" & ChrW(&HE9)).IndexOf("e"c, StringComparison.Ordinal)) ' "résumé": prints '1'
    Console.WriteLine(("re" & ChrW(&H301) & "sume" & ChrW(&H301)).IndexOf("e"c, StringComparison.Ordinal)) ' "résumé": prints '1'
    Console.WriteLine("resume".IndexOf("e"c, StringComparison.OrdinalIgnoreCase)) ' "resume": prints '1'
    Console.WriteLine(("r" & ChrW(&HE9) & "sum" & ChrW(&HE9)).IndexOf("e"c, StringComparison.OrdinalIgnoreCase)) ' "résumé": prints '-1'
    Console.WriteLine(("r" & ChrW(&HE9) & "sume" & ChrW(&H301)).IndexOf("e"c, StringComparison.OrdinalIgnoreCase)) ' "résumé": prints '5'
    Console.WriteLine(("re" & ChrW(&H301) & "sum" & ChrW(&HE9)).IndexOf("e"c, StringComparison.OrdinalIgnoreCase)) ' "résumé": prints '1'
    Console.WriteLine(("re" & ChrW(&H301) & "sume" & ChrW(&H301)).IndexOf("e"c, StringComparison.OrdinalIgnoreCase)) ' "résumé": prints '1'
End Sub

序数検索ルーチンと比較ルーチンは、現在のスレッドのカルチャ設定の影響を受けることはありません。

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

Von Bedeutung

文字列比較メソッドでは、埋め込まれた 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 文字を扱う場合、このポリシーは通常の ASCII 大文字と小文字の区別を無視する点を除き、 StringComparison.Ordinalと同じです。 したがって、[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 つの値のいずれかを使用します。 ただし、バイト単位の比較を実行するため、言語的な並べ替え順序 (英語の辞書など) ではなく、バイナリの並べ替え順序で並べ替えられます。 ユーザーに表示される場合、ほとんどのコンテキストで結果が奇数に見える場合があります。

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

言語文字列の比較

言語検索 および比較ルーチンは、文字列を 照合順序要素 に分解し、これらの要素に対して検索または比較を実行します。 文字列の文字とその構成照合順序要素の間に必ずしも 1 対 1 のマッピングがあるとは限りません。 たとえば、長さ 2 の文字列は、1 つの照合順序要素のみで構成できます。 言語対応の方法で 2 つの文字列が比較される場合、比較子は、文字列のリテラル文字が異なる場合でも、2 つの文字列の照合順序要素が同じセマンティック意味を持っているかどうかを確認します。

文字列 "résumé" と、前のセクションで説明した 4 つの異なる表現について考えてみましょう。 次の表は、照合順序要素に分割された各表現を示しています。

照合要素として
"r\u00E9sum\u00E9" "r" + "\u00E9" + "s" + "u" + "m" + "\u00E9"
"r\u00E9sume\u0301" "r" + "\u00E9" + "s" + "u" + "m" + "e\u0301"
"re\u0301sum\u00E9" "r" + "e\u0301" + "s" + "u" + "m" + "\u00E9"
"re\u0301sume\u0301" "r" + "e\u0301" + "s" + "u" + "m" + "e\u0301"

照合順序要素は、リーダーが 1 文字または文字のクラスターと考える内容に大まかに対応します。 概念的には grapheme クラスター に似ていますが、やや大きな傘が含まれます。

言語比較子では、完全一致は必要ありません。 照合順序要素は、代わりにセマンティックの意味に基づいて比較されます。 たとえば、言語比較子は、部分文字列の"\u00E9""e\u0301"を等しいものとして扱います。これは、両方とも意味的に "鋭いアクセント修飾子を持つ小文字の e" を意味するためです。これにより、IndexOf メソッドは、次のコード サンプルに示すように、セマンティックに等価な部分文字列"e\u0301"を含む大きな文字列内の部分文字列"\u00E9"と一致させることができます。

Console.WriteLine("r\u00E9sum\u00E9".IndexOf("e")); // "résumé": prints '-1' (not found)
Console.WriteLine("r\u00E9sum\u00E9".IndexOf("\u00E9")); // "résumé": prints '1'
Console.WriteLine("\u00E9".IndexOf("e\u0301")); // prints '0'
Sub IndexOfStringExample()
    Console.WriteLine(("r" & ChrW(&HE9) & "sum" & ChrW(&HE9)).IndexOf("e")) ' "résumé": prints '-1' (not found)
    Console.WriteLine(("r" & ChrW(&HE9) & "sum" & ChrW(&HE9)).IndexOf(ChrW(&HE9).ToString())) ' "résumé": prints '1'
    Console.WriteLine(ChrW(&HE9).ToString().IndexOf("e" & ChrW(&H301))) ' prints '0'
End Sub

その結果、言語比較を使用する場合、長さが異なる 2 つの文字列が等しいと比較される可能性があります。 呼び出し元は、このようなシナリオで文字列の長さを処理する特別なロジックを考慮しないよう注意する必要があります。

カルチャに対応した 検索および比較ルーチンは、言語検索および比較ルーチンの特殊な形式です。 カルチャ対応の比較子では、照合順序要素の概念が拡張され、指定したカルチャに固有の情報が含まれます。

たとえば、 ハンガリー語のアルファベットでは、2 文字 <dz> が連続して表示される場合、 <d> または <z> とは異なる独自の一意の文字と見なされます。 つまり、 <dz> が文字列で見られる場合、ハンガリー語のカルチャに対応した比較子は、それを単一の照合順序要素として扱います。

照合順序要素として 注釈
"endz" "e" + "n" + "d" + "z" (標準言語比較子を使用)
"endz" "e" + "n" + "dz" (ハンガリー語のカルチャに対応した比較子を使用)

ハンガリー語のカルチャ対応比較子を使用する場合、文字列"endz"部分文字列で終"z"。これは、<dz> と <z> が異なるセマンティック意味を持つ照合順序要素と見なされるためです。

// Set thread culture to Hungarian
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("hu-HU");
Console.WriteLine("endz".EndsWith("z")); // Prints 'False'

// Set thread culture to invariant culture
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Console.WriteLine("endz".EndsWith("z")); // Prints 'True'
' Set thread culture to Hungarian
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("hu-HU")
Console.WriteLine("endz".EndsWith("z")) ' Prints 'False'

' Set thread culture to invariant culture
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture
Console.WriteLine("endz".EndsWith("z")) ' Prints 'True'

  • 動作: 言語および文化に対応した比較機能は、動作の調整が行われることがあります。 ICU と以前の Windows NLS 機能の両方が、世界言語の変化を考慮して更新されます。 詳細については、ブログ投稿 ロケール (カルチャ) データの変動を参照してください。 序数比較子の動作は、ビットごとの正確な検索と比較を実行するため、変更されることはありません。 ただし、Unicode がより多くの文字セットを含むように拡大し、既存の大文字と小文字データの欠落を補完すると、OrdinalIgnoreCase 比較子の動作が変わる可能性があります。
  • 使用法: 比較子 StringComparison.InvariantCultureStringComparison.InvariantCultureIgnoreCase は、カルチャに対応していない言語的比較子です。 つまり、これらの比較子は、複数の可能な基になる表現を持つアクセント付き文字 é などの概念を理解し、そのような表現はすべて等しく扱う必要があります。 ただし、カルチャに対応していない言語比較子には、上に示すように、<dz>、<d>、または<z>を区別するための特別な処理は含まれていません。 また、ドイツ語の Eszett (ß) のような特殊な文字は使用されません。

.NET では、 インバリアント グローバリゼーション モードも提供されます。 このオプトイン モードでは、言語検索と比較ルーチンを扱うコード パスが無効になります。 このモードでは、CultureInfoStringComparisonの引数で呼び出し元が指定する内容に関係なく、すべての操作ではOrdinalまたはOrdinalIgnoreCaseの動作を使用します。 詳細については、「グローバリゼーションと .NET Core グローバリゼーション インバリアント モードのランタイム構成オプション」を参照してください。

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

インバリアント カルチャとの比較では、静的CompareInfo プロパティによって返されるCultureInfo.InvariantCulture プロパティが使用されます。 この動作はすべてのシステムで同じです。それは、その範囲外の文字を同等の不変文字であると信じるものに変換します。 このポリシーは、カルチャ間で 1 つの文字列の動作セットを維持する場合に役立ちますが、多くの場合、予期しない結果が得られます。

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

StringComparison.InvariantCultureStringComparison.Ordinalを使用する比較は、ASCII 文字列でも同じように機能します。 ただし、 StringComparison.InvariantCulture は、一連のバイトとして解釈する必要がある文字列には適さない言語的な決定を行います。 CultureInfo.InvariantCulture.CompareInfo オブジェクトを使用すると、Compareメソッドは特定の文字セットを等価として解釈します。 たとえば、インバリアント カルチャでは、次の等価性が有効です。

インバリアント カルチャ: 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 {separated} and {combined} using InvariantCulture: {string.Compare(separated, combined, StringComparison.InvariantCulture) == 0}");

Console.WriteLine($"Equal sort weight of {separated} and {combined} using Ordinal: {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

ファイル名、Cookie、または "å" などの組み合わせが表示されるその他の何かを解釈する場合でも、序数の比較では最も透過的で適切な動作が提供されます。

バランスの取れたインバリアント カルチャには、比較に役立つプロパティはほとんどありません。 言語的に関連する方法で比較を行います。これにより、完全なシンボリック等価性が保証されるのを防ぐことができますが、どのカルチャでも表示する選択ではありません。 比較に StringComparison.InvariantCulture を使用するいくつかの理由の 1 つは、異文化同一の表示に対して順序付けされたデータを保持することです。 たとえば、表示用の並べ替えられた識別子のリストを含む大きなデータ ファイルがアプリケーションに付属している場合、このリストに追加するには、インバリアント スタイルの並べ替えを使用して挿入する必要があります。

StringComparison メンバーを選択する方法

次の表は、セマンティック文字列コンテキストから StringComparison 列挙メンバーへのマッピングの概要を示しています。

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

価値
ケースに依存する内部識別子。

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

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

XML や HTTP などの標準では、大文字と小文字を区別しない識別子。

ファイル パス。

レジストリ キーと値。

環境変数。

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

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

固定の並べ替え順序を必要とする言語データの表示。
言語的に関連性がある、文化的に依存しないデータ。 InvariantCulture

-又は-

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

ほとんどのユーザー入力。
現地の言語習慣を必要とするデータ。 CurrentCulture

-又は-

CurrentCultureIgnoreCase

セキュリティへの影響

アプリでフィルター処理またはアクセス制御に文字列 API を使用する場合は、序数比較を使用します。 現在のカルチャに基づく言語比較では、プラットフォームとロケールによって異なる予期しない結果が発生する可能性があります。 次のようなコード パターンは、セキュリティの悪用の影響を受ける可能性があります。

//
// THIS SAMPLE CODE IS INCORRECT.
// DO NOT USE IT IN PRODUCTION.
//
bool ContainsHtmlSensitiveCharacters(string input)
{
    if (input.IndexOf("<") >= 0) { return true; }
    if (input.IndexOf("&") >= 0) { return true; }
    return false;
}
'
' THIS SAMPLE CODE IS INCORRECT.
' DO NOT USE IT IN PRODUCTION.
'
Function ContainsHtmlSensitiveCharacters(input As String) As Boolean
    If input.IndexOf("<") >= 0 Then Return True
    If input.IndexOf("&") >= 0 Then Return True
    Return False
End Function

string.IndexOf(string) メソッドは既定で言語検索を使用するため、文字列にリテラル'<'または'&'文字を含め、検索部分文字列が見つからなかったことを示すstring.IndexOf(string)-1を返す可能性があります。 コード分析ルール CA1307 と CA1309 は、このような呼び出しサイトにフラグを設定し、潜在的な問題があることを開発者に警告します。

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

次のセクションでは、文字列比較に最も一般的に使用されるメソッドについて説明します。

String.Compare

既定の解釈: StringComparison.CurrentCulture

文字列の解釈の最も中心となる操作として、これらのメソッド呼び出しのすべてのインスタンスを調べて、文字列を現在のカルチャに従って解釈するか、カルチャから関連付けを解除するか (シンボリックに) 判断する必要があります。 通常は後者であり、代わりに StringComparison.Ordinal 比較を使用する必要があります。

System.Globalization.CompareInfo プロパティによって返される CultureInfo.CompareInfo クラスには、Compare フラグ列挙によって多数の一致オプション (序数、空白の無視、かな型の無視など) を提供するCompareOptions メソッドも含まれています。

String.CompareTo

既定の解釈: StringComparison.CurrentCulture

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

IComparableインターフェイスとIComparable<T> インターフェイスを実装する型は、このメソッドを実装します。 StringComparison パラメーターのオプションは提供されないため、型を実装すると、多くの場合、ユーザーはコンストラクターでStringComparerを指定できます。 次の例では、クラス コンストラクターに FileName パラメーターが含まれるStringComparer クラスを定義します。 この 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の場合)、または StringComparison.Ordinal (最初のパラメーターが charの場合)。

これらのメソッドの既定のオーバーロードが比較を実行する方法に不整合があります。 char パラメーターを受け取るオーバーロードは序数比較を実行しますが、string パラメーターを受け取るオーバーロードはカルチャに依存する比較を実行し、印刷されない文字を無視する可能性があります。

String.IndexOf および String.LastIndexOf

既定の解釈: StringComparison.CurrentCulture

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

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

String.Contains

既定の解釈: StringComparison.Ordinal

String.IndexOfとは異なり、String.Contains メソッドでは、charstringの両方のオーバーロードに対して既定で序数比較が使用されます。 ただし、呼び出しサイトで動作を明確にするために、意図が重要な場合は明示的な StringComparison 引数を渡す必要があります。

MemoryExtensions.AsSpan.IndexOfAnySearchValues<T>

.NET 8 では、 SearchValues<T> 型が導入されました。これは、スパン内の特定の文字セットまたはバイトを検索するための最適化されたソリューションを提供します。

文字列を固定の既知の値のセットと繰り返し比較する場合は、チェーン比較や LINQ ベースのアプローチではなく、 SearchValues<T>.Contains(T) メソッドを使用することを検討してください。 SearchValues<T> では、内部参照構造を事前計算し、指定された値に基づいて比較ロジックを最適化できます。 パフォーマンスの利点を確認するには、 SearchValues<string> インスタンスを 1 回作成してキャッシュしてから、比較のために再利用します。

using System.Buffers;

namespace ExampleCode;

internal partial class DemoCode
{
    private static readonly SearchValues<string> Commands =
        SearchValues.Create(
            ["start", "run", "go", "begin", "commence"],
            StringComparison.OrdinalIgnoreCase);

    void ProcessCommand(string command)
    {
        if (Commands.Contains(command))
        {
            // ...
        }
    }
}
Imports System.Buffers

Namespace ExampleCode
    Partial Friend Class DemoCode

        Private Shared ReadOnly Commands As SearchValues(Of String) =
            SearchValues.Create(
                {"start", "run", "go", "begin", "commence"},
                StringComparison.OrdinalIgnoreCase)

        Sub ProcessCommand(command As String)
            If Commands.Contains(command) Then
                ' ...
            End If
        End Sub

    End Class
End Namespace

.NET 9 では、 SearchValues が拡張され、より大きな文字列内の部分文字列の検索がサポートされました。 例については、拡張SearchValues参照してください。

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

一元的な操作として文字列比較を行う一部の文字列以外のメソッドでは、 StringComparer 型が使用されます。 StringComparer クラスには、StringComparer メソッドが次の種類の文字列比較を実行StringComparer.Compareインスタンスを返す 6 つの静的プロパティが含まれています。

Array.Sort および Array.BinarySearch

既定の解釈: StringComparison.CurrentCulture

コレクションにデータを格納したり、ファイルまたはデータベースから永続化されたデータをコレクションに読み取ったりすると、現在のカルチャを切り替えると、コレクション内のインバリアントが無効になることがあります。 Array.BinarySearchメソッドは、検索対象の配列内の要素が既に並べ替えられていることを前提としています。 配列内の任意の文字列要素を並べ替えるために、 Array.Sort メソッドは String.Compare メソッドを呼び出して個々の要素を並べ替えます。 カルチャに依存する比較子を使用すると、配列の並べ替えと内容の検索の間でカルチャが変化すると、危険な場合があります。 たとえば、次のコードでは、ストレージと取得は、 Thread.CurrentThread.CurrentCulture プロパティによって暗黙的に提供される比較子に対して動作します。 StoreNamesDoesNameExistの呼び出しの間でカルチャが変更される可能性がある場合、特に配列の内容が 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 ALine 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 番目の例を提供します。

次の例では、Hashtable プロパティによって返されるStringComparer オブジェクトを渡すことによって、StringComparer.OrdinalIgnoreCase オブジェクトをインスタンス化します。 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

コレクションの例: SortedSet<T>List<T>.Sort

並べ替えられた文字列のコレクションをインスタンス化する場合や、既存の文字列ベースのコレクションを並べ替える場合も、同じロケール感度の問題が適用されます。 常に明示的な比較子を指定します。

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

//
// Potentially incorrect code - behavior might vary based on locale.
//
SortedSet<string> mySet = [.. values]; // No comparer specified

List<string> list = [.. values];
list.Sort(); // No comparer specified

//
// Corrected code - uses ordinal sorting; doesn't vary by locale.
//
SortedSet<string> mySet2 = new(values, StringComparer.Ordinal);

List<string> list2 = [.. values];
list2.Sort(StringComparer.Ordinal);
' Words to sort
Dim values As String() = {"able", "ångström", "apple", "Æble",
                          "Windows", "Visual Studio"}

'
' Potentially incorrect code - behavior might vary based on locale.
'
Dim mySet As New SortedSet(Of String)(values) ' No comparer specified

Dim list As New List(Of String)(values)
list.Sort() ' No comparer specified

'
' Corrected code - uses ordinal sorting; doesn't vary by locale.
'
Dim mySet2 As New SortedSet(Of String)(values, StringComparer.Ordinal)

Dim list2 As New List(Of String)(values)
list2.Sort(StringComparer.Ordinal)

.NET と .NET Framework の違い

.NET と .NET Framework では、グローバリゼーションの処理方法が異なります。 Windows 上の .NET Framework では、言語文字列の比較にオペレーティング システムの 各国語サポート (NLS) 機能が使用されます。 .NET では、サポートされているすべてのプラットフォームで言語的な文字列比較に Unicode (ICU) 用の International Components (International Components) ライブラリを使用します。

ICU と NLS は言語比較子に異なるロジックを実装するため、カルチャに依存する比較を使用する文字列メソッドの結果は、.NET と .NET Framework で異なる場合があります。 これは、次のような言語比較子を既定で使用するメソッドに重要です。

これは、影響を受ける API の完全な一覧ではありません。

主な違いの 1 つは、埋め込み null とその他の制御文字の処理です。 NLS で言語比較子を使用する場合、null 文字 (\0) などの一部の制御文字は、特定の比較コンテキストで無視可能として扱われる場合があります。 ICU では、これらの文字は文字列内で実際の文字として扱われます。 これにより、検索文字列に null 文字が含まれている場合に、 string.IndexOf(string) が異なる結果を返す可能性があります。

たとえば、次のコードは、現在のランタイムに応じて異なる回答を生成できます。

const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");

// The snippet prints:
//
// '3' when running on .NET Framework and .NET Core 2.x - 3.x (Windows)
// '0' when running on .NET 5 or later (Windows)
// '0' when running on .NET Core 2.x - 3.x or .NET 5 (non-Windows)
// '3' when running on .NET Core 2.x or .NET 5+ (in invariant mode)
Const greeting As String = "Hel" & vbNullChar & "lo"
Console.WriteLine($"{greeting.IndexOf(CStr(vbNullChar))}")

' The snippet prints:
'
' '3' when running on .NET Framework and .NET Core 2.x - 3.x (Windows)
' '0' when running on .NET 5 or later (Windows)
' '0' when running on .NET Core 2.x - 3.x or .NET 5 (non-Windows)
' '3' when running on .NET Core 2.x or .NET 5+ (in invariant mode)

これらのクロスプラットフォームおよびクロス実装の驚きを回避する最善の方法は、文字列比較メソッドに明示的な StringComparison 引数を常に渡し、非言語比較に StringComparison.Ordinal または StringComparison.OrdinalIgnoreCase を使用することです。

アプリケーションを .NET Framework から .NET に移行し、Windows 上の従来の NLS 動作に依存している場合は、NLS を使用するようにアプリケーションを構成できます。 詳細については、「 .NET のグローバリゼーションと ICU」を参照してください。

こちらも参照ください