共用方式為


比較 .NET 中字串的最佳做法

.NET 提供廣泛的支持來開發本地化和全球化的應用程式,並讓您在執行排序和顯示字串等常見作業時,輕鬆地套用目前文化特性或特定文化特性的慣例。 但是,排序或比較字串不一定是區分文化特性的作業。 例如,應用程式在內部使用的字串通常應該在所有文化背景中統一處理。 當不受文化差異影響的字串資料,例如 XML 標記、HTML 標記、使用者名稱、檔案路徑和系統對象的名稱,被當作對文化差異敏感來解讀時,應用程式程式代碼可能導致細微錯誤、效能不佳,甚至在某些情況下會引發安全性問題。

本文會檢查 .NET 中的字串排序、比較和大小寫方法、提供選取適當字串處理方法的建議,並提供字串處理方法的其他資訊。

字串使用方式的建議

當您使用 .NET 進行開發時,請在比較字串時遵循這些建議。

小提示

各種字串相關方法會執行比較。 範例包括 String.EqualsString.CompareString.IndexOfString.StartsWith

當您比較字串時,請避免下列做法:

  • 請勿使用未明確或隱含指定字串操作之字串比較規則的重載。
  • 在大部分情況下,請勿使基於 StringComparison.InvariantCulture 的字串操作。 其中一個例外是,當您保存語言上有意義但文化上無關的資料時。
  • 請勿使用 String.CompareCompareTo 方法的多載,並且避免通過測試零的傳回值來判斷兩個字串是否相等。

明確指定字串比較

.NET 中的大部分字串操作方法都會多載(overloaded)。 一般而言,一或多個多載會接受預設設定,而其他多載則不接受預設值,而是定義要比較或作字串的精確方式。 不依賴預設值的大多數方法都包含 類型的 StringComparison參數,這是列舉,明確指定文化特性和大小寫的字串比較規則。 下表描述 StringComparison 列舉成員。

StringComparison 成員 說明
CurrentCulture 使用目前的文化特性執行區分大小寫的比較。
CurrentCultureIgnoreCase 使用目前的文化特性執行不區分大小寫的比較。
InvariantCulture 使用不因文化特性而異來執行區分大小寫的比較。
InvariantCultureIgnoreCase 在不變文化的背景下執行不區分大小寫的比較。
Ordinal 執行序數比較。
OrdinalIgnoreCase 執行不區分大小寫的序數比較。

例如, IndexOf 方法具有九種多載,它會傳回 String 物件中符合字元或字串的子字串索引。

基於下列原因,建議您選取不使用預設值的多載:

  • 某些具有預設參數的多載(在字串實例中搜尋 Char 的多載)會執行序數比較,而另一些(在字串實例中搜尋字串的多載)則依據文化特性進行比較。 很難記住哪個方法會使用哪一個預設值,而且很容易混淆多載。

  • 目前還不清楚依賴方法呼叫預設值的程式代碼意圖。 在下列依賴預設值的範例中,很難知道開發人員實際上是否想要對兩個字符串進行序數比較或語言比較,或是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

字串比較的詳細數據

字串比較是許多字串相關作業的核心,特別是排序和測試是否相等。 字串會以已決定的順序排序:如果 「my」 出現在字串排序列表中的 「string」 之前,「my」 必須比較小於或等於 「string」。 此外,比較會隱含地定義相等。 比較作業會針對它認為相等的字串傳回零。 良好的解譯是,兩個字串都不小於另一個字串。 涉及字串的最有意義的作業包括下列其中一個或兩個程式:與另一個字串比較,以及執行定義完善的排序作業。

備註

您可以下載 排序權數數據表、一組文本檔,其中包含 Windows作系統排序和比較作業中使用的字元權數資訊,以及 Linux 和 macOS 最新版排序權數 數據表的 Unicode 定序元素數據表。 Linux 和 macOS 上排序權數數據表的特定版本取決於系統上所安裝 Unicode 連結庫的國際元件 版本。 如需ICU版本及其實作的Unicode版本資訊,請參閱 下載ICU

不過,評估兩個字串是否相等或排序順序不會產生單一、正確的結果;結果取決於用來比較字串的準則。 特別是,根據目前文化特性的大小寫和排序慣例或不因地區設定而異的文化 特性(根據 英文語言的地區設定文化特性)而進行序數或排序慣例的字串比較可能會產生不同的結果。

此外,使用不同版本的 .NET 或在不同作業系統或作業系統版本上使用 .NET 的字串比較可能會傳回不同的結果。 如需詳細資訊,請參閱 字串和 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 參數的多載,讓方法呼叫的意圖清楚。

當非語言字串數據被以語言方式解讀,或當來自特定文化的字串數據被按照另一種文化的慣例解釋時,可能會產生一些微妙或不那麼微妙的錯誤。 標準範例是 Turkish-I 問題。

對於幾乎所有拉丁字母,包括美式英文,字元 「i」 (\u0069) 是字元 「I」 (\u0049) 的小寫版本。 此大小寫規則會很快成為這類文化特性中某人程序設計的默認規則。 然而,土耳其語字母表(“tr-TR”)包含“上有點的 I” 字元 “İ”(\u0130),這是 “i” 的大寫版本。 土耳其文中還包含一個不含點的小寫字母“ı” (\u0131),其大寫形式為“I”。 這種行為也發生在亞塞拜然文化中。

因此,關於將「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.OrdinalStringComparison.OrdinalIgnoreCase 值,表示這是一種忽略自然語言特性的非語言比較。 使用這些 StringComparison 值所叫用的方法,會根據簡單的位元組比較進行基於字串操作的決策,而不是依文化特性參數化的大小寫或等同表格。 在大部分情況下,這種方法最符合字串的預期解譯,同時讓程式代碼更快速且更可靠。

序數比較是字串比較,其中每個字串的每一個字節在沒有語言解譯的情況下進行比較;例如,“windows” 不符合 “Windows”。 這基本上是 C 運行時間 strcmp 函式的呼叫。 當內容指出字串應該完全符合或要求保守比對原則時,請使用此比較。 此外,序數比較是最快的比較作業,因為它在判斷結果時不會套用任何語言規則。

.NET 中的字串可以包含內嵌的 Null 字元(以及其他非列印字元)。 序數與區分文化特性的比較(包括使用非變異文化特性的比較)之間最明顯的差異之一,就是處理字串中內嵌的 Null 字元。 當您使用 String.CompareString.Equals 方法來執行區分文化特性的比較時,會忽略這些字元(包括使用不因文化特性而異的比較)。 因此,包含內嵌 Null 字元的字串可以視為等於不內嵌 Null 字元的字串。 基於字串比較方法的目的,可能會略過內嵌的非列印字元,例如 String.StartsWith

這很重要

雖然字串比較方法會忽略內嵌的 Null 字元,但字串搜尋方法,例如 String.ContainsString.EndsWithString.IndexOfString.LastIndexOfString.StartsWith 則不會。

下列範例進行對字串「Aa」的文化敏感性比較,該字串有一個類似字串,字串「A」和「a」之間包含數個內嵌空字符,並顯示這兩個字串如何被視為相等。

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都直接使用二進位值,並且最適合用來比對。 當您不確定比較設定時,請使用這兩個值的其中一個。 不過,因為它們會執行位元組位元組比較,所以不會依語言排序順序(例如英文字典),而是依二進位排序順序排序。 如果向用戶顯示,結果在大多數情況下看起來可能很奇怪。

未包含String.Equals參數的StringComparison多載的預設值是序數語義(包括等號運算元)。 在任何情況下,我們建議您呼叫具有 StringComparison 參數的重載。

使用不因文化特性而異的字串操作

與不變文化的比較使用靜態 CompareInfo 屬性所回傳的 CultureInfo.InvariantCulture 屬性。 此行為在所有系統上都相同;它會將範圍以外的任何字元轉譯成其認為相等不變的字元。 此原則對於跨文化特性維護一組字串行為很有用,但通常會提供非預期的結果。

不區分大小寫且不受文化特性影響的比對,也會使用靜態CompareInfo屬性所傳回的CultureInfo.InvariantCulture屬性來取得比對資訊。 這些轉譯字元之間的任何大小寫差異會被忽略。

使用StringComparison.InvariantCultureStringComparison.Ordinal 的比較在 ASCII 字串上運作方式相同。 不過, StringComparison.InvariantCulture 進行語言決策,可能不適合必須解譯為位元組集的字串。 物件 CultureInfo.InvariantCulture.CompareInfo 會讓方法將 Compare 特定字元集解譯為對等字元集。 例如,在非變異文化特性下,下列等價有效:

InvariantCulture: a + ̊ = å

拉丁小寫字母“a”(\u0061),當它位於組合環上方字元“+” ̊(\u030a)時,會被解讀為拉丁小寫字母加上環字元“å”(\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 成員

下表概述語意字串內容到 StringComparison 列舉成員的對應映射:

資料 行為 對應的 System.StringComparison

價值
區分大小寫的內部識別碼。

XML 和 HTTP 等標準中的區分大小寫識別碼。

區分大小寫的安全性相關設定。
非語言標識碼,其中位元組完全相符。 Ordinal
不區分大小寫的內部識別碼。

XML 和 HTTP 等標準中的不區分大小寫標識碼。

檔案路徑。

登錄機碼和值。

環境變數。

資源識別碼(例如句柄名稱)。

不區分大小寫的安全性相關設定。
非語言標識碼,其中案例無關。 OrdinalIgnoreCase
某些保存的語言相關數據。

顯示需要固定排序順序的語言數據。
語言上依然相關但在文化上不具特定性的數據。 InvariantCulture

-或-

InvariantCultureIgnoreCase
向用戶顯示的數據。

大部分的用戶輸入。
需要符合當地語言慣例的數據。 CurrentCulture

-或-

CurrentCultureIgnoreCase

.NET 中的常見字串比較方法

下列各節說明最常用於字串比較的方法。

String.Compare (字串比較)

默認解譯: StringComparison.CurrentCulture

作為字串解譯最核心的作業,應該檢查這些方法呼叫的所有實例,以判斷字串是否應該根據目前的文化特性來解譯,或與文化特性分離(以符號方式)。 一般而言,這是後者,而 StringComparison.Ordinal 應該改用比較。

屬性 System.Globalization.CompareInfo 所傳回的 CultureInfo.CompareInfo 類別也包含 Compare 方法,這個方法會透過 CompareOptions 旗標列舉來提供大量的比對選項(序數、忽略空格符、忽略假名類型等等)。

String.CompareTo

默認解譯: StringComparison.CurrentCulture

目前此方法不提供指定 StringComparison 類型的多載功能。 通常可以將此方法轉換成建議 String.Compare(String, String, StringComparison) 表單。

實作 IComparableIComparable<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.ToUpperInvariantString.ToLowerInvariant 方法也可供使用。 ToUpperInvariant 是標準化大小寫的常用方法。 使用 StringComparison.OrdinalIgnoreCase 進行的比較在行為上是由兩次呼叫組成:對這兩個字串參數進行 ToUpperInvariant 呼叫,然後使用 StringComparison.Ordinal 進行比較。

在特定文化中,有多載可用來轉換成大寫或小寫,方法是將代表該文化的 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

這些方法的預設多載執行比較的方式缺乏一致性。 包含String.IndexOf參數的所有 String.LastIndexOfChar 方法都會執行序數比較,但包含String.IndexOf參數的預設String.LastIndexOfString方法會執行區分文化特性的比較。

如果您呼叫 String.IndexOf(String)String.LastIndexOf(String) 方法,並在目前實例中傳遞要定位的字串,我們建議您呼叫明確指定StringComparison型別的多載版本。 包含 Char 自變數的多載不允許您指定 StringComparison 類型。

間接執行字串比較的方法

某些以字串比較為核心操作的非字串方法會使用 StringComparer 類型。 類別 StringComparer 包含六個靜態屬性,這些屬性會傳回 StringComparer 方法執行下列字串比較類型的實例 StringComparer.Compare

Array.Sort 和 Array.BinarySearch

默認解譯: StringComparison.CurrentCulture

當您將任何數據儲存在集合中,或將保存的數據從檔案或資料庫讀取至集合時,切換目前的文化特性可能會使集合中的不變異失效。 方法 Array.BinarySearch 假設要搜尋的陣列中的元素已經排序。 若要排序陣列中的任何字串專案, Array.Sort 方法會呼叫 String.Compare 方法來排序個別專案。 如果在排序陣列和搜尋其內容之間文化發生變化,則使用區分文化的比較器可能會很危險。 例如,在下列程式碼中,儲存和擷取會在由 Thread.CurrentThread.CurrentCulture 屬性隱含提供的比較子上運作。 如果在StoreNamesDoesNameExist之間的呼叫過程中文化特性發生變更,尤其是當陣列內容在兩次方法呼叫之間的某個位置被持久化保存時,二進位搜尋可能會失敗。

// 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

下列範例中會出現建議的變化,它會使用相同的序數(不區分文化特性)比較方法來排序和搜尋數位列。 變更程式代碼會反映在加上 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,這會以語言方式運作,以提供更好的用戶輸出,但不受文化特性變更影響。 下列範例修改前兩個範例,以使用不因文化差異而異的文化來排序和搜尋數組。

// 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屬性傳回的StringComparer物件,來具現化StringComparer.OrdinalIgnoreCase物件。 由於從 StringComparer 衍生出來的類別StringComparer實作IEqualityComparer介面,因此其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

另請參閱