比较 .NET 中的字符串的最佳做法

.NET 为开发本地化和全球化的应用程序提供了广泛的支持,在执行排序和显示字符串等常见操作时,可以轻松应用当前文化或特定文化的约定。 但是,排序或比较字符串并不总是具有文化敏感性。 例如,对于应用程序内部使用的字符串,通常应该跨所有区域性以相同的方式对其进行处理。 如果将 XML 标记、HTML 标记、用户名、文件路径和系统对象名称等与区域性无关的字符串数据解释为区分区域性,则应用程序代码会遭遇细微的错误、不佳的性能,在某些情况下,还会遭遇安全性问题。

本文介绍 .NET 中的字符串排序、比较和大小写方法,提供了有关选择适当字符串处理方法的建议,并提供有关字符串处理方法的其他信息。

有关字符串用法的建议

使用 .NET 进行开发时,请在比较字符串时遵循这些建议。

小窍门

各种与字符串相关的方法执行比较。 示例包括 String.EqualsString.CompareString.IndexOfString.StartsWith

比较字符串时,请避免以下做法:

  • 不要使用未显式或隐式为字符串操作指定字符串比较规则的重载。
  • 在大多数情况下不要基于 StringComparison.InvariantCulture 使用字符串操作。 少数例外之一是在持久保存语言上有意义但与文化无关的数据时。
  • 请勿使用String.CompareCompareTo方法的重载,并通过测试返回值是否为零来确定两个字符串是否相等。

显式指定字符串比较

.NET 中的大多数字符串操作方法都是重载的。 通常,一个或多个重载接受默认设置,而其他重载则不接受默认值,而是定义要比较或处理字符串的精准方式。 大多数不依赖于默认设置的方法都包括 StringComparison 类型的参数,该参数是按区域性和大小写为字符串比较显式指定规则的枚举。 下表描述了 StringComparison 枚举成员。

StringComparison 成员 DESCRIPTION
CurrentCulture 使用当前区域性执行区分大小写的比较。
CurrentCultureIgnoreCase 使用当前区域性执行不区分大小写的比较。
InvariantCulture 使用固定区域性执行区分大小写的比较。
InvariantCultureIgnoreCase 使用固定区域性执行不区分大小写的比较。
Ordinal 执行序号比较。
OrdinalIgnoreCase 执行不区分大小写的序号比较。

例如, IndexOf 该方法返回与字符或字符串匹配的对象中的 String 子字符串的索引,具有 9 个重载:

我们建议选择不使用默认值的重载,原因如下:

  • 具有默认参数的一些重载(在字符串实例中搜索 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 操作系统的排序和比较操作中使用的字符权重的信息,以及 默认的 Unicode 排序规则元素表,即适用于 Linux 和 macOS 的最新版本的排序权重表。 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”的大写形式。 土耳其语还包括小写的“i 没有点”字符,“ı”(\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.OrdinalStringComparison.OrdinalIgnoreCase 值表示非语言比较,这种比较忽略了自然语言的特性。 利用 StringComparison 值调用的方法将字符串操作决策建立在简单的字节比较的基础之上,而不是按区域性参数化的大小写或相等表。 在大多数情况下,此方法最适合字符串的预期解释,同时使代码更快、更可靠。

序号比较就是字符串比较,在这种比较中,将比较每个字符串中的每个字节且不进行语言解释;例如,“windows”不匹配“Windows”。 这实质上是对 C 运行时 strcmp 函数的调用。 当上下文指示应完全匹配字符串或要求保守匹配策略时,请使用此比较。 此外,序号比较是最快的比较作,因为它在确定结果时不应用任何语言规则。

.NET 中的字符串可以包含嵌入的 null 字符(以及其他非打印字符)。 顺序比较和文化敏感比较(包括使用不变文化的比较)之间最明显的差异之一涉及如何处理字符串中嵌入的 null 字符。 当使用 String.CompareString.Equals 方法执行区分区域性的比较(包括使用固定区域性的比较)时,将忽略这些字符。 因此,包含嵌入 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 该方法将某些字符集解释为等效字符集。 例如,以下等效性在不变文化下有效:

固定Culture:a + ̊ = å

如果 A 字符的小写拉丁字母“a”(\u0061) 旁边有上方组合圆圈字符“+ " ̊”(\u030a),A 字符就会被解释为,上方带有圆圈的小写拉丁字母“å”(\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 比较的一个理由是保存排序后数据,以确保跨文化的一致显示。 例如,如果一个包含已排序的标识符列表的大型数据文件与应用程序一起使用,那么要向此列表添加内容时,需要进行保持排序不变的插入操作。

为方法调用选择 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.ToUpperInvariant这些方法和String.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.LastIndexOf方法和Char方法执行序数比较,但包含String.IndexOf参数的默认String.LastIndexOfString方法执行文化敏感比较。

如果调用 String.IndexOf(String)String.LastIndexOf(String) 方法,并向其传递一个要在当前实例中定位的字符串,我们建议调用一个显式指定 StringComparison 类型的重载方法。 包含 Char 参数的重载不允许指定类型 StringComparison

间接执行字符串比较的方法

将字符串比较作为中心作的一些非字符串方法使用类型 StringComparer 。 该 StringComparer 类包括六个静态属性,这些属性返回 StringComparerStringComparer.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

另请参阅