有关在 Microsoft .NET 2.0 中使用字符串的新建议

 

戴夫·费特曼
Microsoft Corporation

2005 年 5 月

适用于:
   Microsoft .NET 2.0
   Microsoft .NET Framework

总结: 以前使用 InvariantCulture 进行字符串比较、大小写和排序的代码所有者强烈建议考虑在 Microsoft .NET 2.0 中使用一组新的字符串重载。 具体而言,设计为与区域性无关且在语言上无关的数据应开始使用新的 StringComparison 枚举的 StringComparison.OrdinalStringComparison.OrdinalIgnoreCase 成员来指定重载。 它们强制实施类似于 strcmp 的逐字节比较,不仅避免了对本质上是符号字符串的语言解释的 bug,而且提供更好的性能。 ) (15 个打印页

目录

简介
字符串使用建议
新类型的概述和基本原理
为方法调用选择 StringComparison 成员
动机:土耳其-我问题
框架中的常见字符串比较方法
用于正确字符串比较的新 Whidbey API 列表
本机代码说明
关于固定区域性的早期建议呢?
结论

简介

借助 Microsoft .NET Framework,开发人员可以通过为当前区域设置正确解释字符串而设计的全面机制,创建完全准备好进行本地化和国际化的软件。 这有助于快速创建和使用专为各种文化设计的解决方案。 但是,当这些方法解释与文化无关的字符串数据时,代码可能会表现出细微的 bug,并在未经测试的区域性上运行速度比必要的慢。

解释字符串时,区域性感知类型的规范示例(有时翻转区域性开关)会导致意外结果。 同一字符串可以在不同的 Thread.CurrentCulture 设置下以不同的方式排序、区分大小写和比较。 有时,应根据用户的区域性 (允许字符串) 显示数据,但对于应用程序内部的大多数字符串(如 XML 标记、用户名、文件路径和系统对象),解释应在所有区域性中保持一致。 此外,当字符串表示此类符号信息时,应完全以非语言方式解释比较操作。

字符串使用建议

使用 2.0 版.NET Framework进行开发时,记住一些非常简单的建议就足以解决有关使用字符串的困惑。

  • DO: 使用 StringComparison.OrdinalOrdinalIgnoreCase 进行比较,作为与区域性无关的字符串匹配的安全默认值。
  • DO: 使用 StringComparison.OrdinalOrdinalIgnoreCase 比较来提高速度。
  • DO: 向用户显示输出时,请使用基于 StringComparison.CurrentCulture 的字符串操作。
  • DO: 将当前基于固定区域性的字符串操作的使用切换为使用非语言的 StringComparison.OrdinalStringComparison.OrdinalIgnoreCase ,当比较在语言上与符号 (不相关时,例如) 。
  • DO: 规范化字符串以进行比较时,请使用 ToUpperInvariant 而不是 ToLowerInvariant
  • 不要: 对未显式或隐式指定字符串比较机制的字符串操作使用重载。
  • 不要: 在大多数情况下,使用基于 StringComparison.InvariantCulture 的字符串操作;少数例外之一是保留语言上有意义但与文化无关的数据。

许多新的和推荐的 String 方法重载都使用 StringComparison 参数,使以下选项变得显式:

示例 1:

String protocol = MyGetUrlProtocol(); 

if (String.Compare(protocol, "ftp", StringComparsion.Ordinal) != 0)
{
   throw new InvalidOperationException();
}

示例 2:

String filename = args[0];
StreamReader reader;

if (String.EndsWith(filename, "txt", StringComparison.OrdinalIgnoreCase))
{
   reader = File.OpenText(filename);   
}

新类型的概述和基本原理

字符串比较是许多与字符串相关的操作的核心,重要的是 排序相等

字符串按确定的顺序排序:如果字符串“my”出现在排序的字符串列表中的“string”之前,则必须在字符串比较中,“my”比较“小于或等于”字符串”。此外,比较也隐式定义 相等性, 因为此比较操作将为其认为相等的任何字符串生成零;一个很好的解释是,这两个字符串都不比另一个字符串“小于”。 涉及字符串的大多数有意义的操作包括以下过程之一或两个过程:与另一个字符串进行比较,以及执行妥善定义的排序。

对于许多重载,Thread.CurrentCulture 规定.NET Framework中的字符串比较的默认行为。 但是,当区域性发生更改时,比较和大小写行为必然会有所不同,无论是在具有与开发代码的区域性不同的计算机上运行,还是执行线程本身更改区域性时。 此行为是有意的,但对许多开发人员来说仍然不明显。

使用现有 API 的新重载以及一些新类型(如 System.StringComparison 枚举),根据不同的区域性信息正确解释字符串变得更加容易。

Whidbey 引入了一个明确的新类型,可缓解围绕正确字符串比较的大部分混淆:mscorlib.dll中的 StringComparison 枚举。

namespace System
{
      public enum StringComparison {
         CurrentCulture,
         CurrentCultureIgnoreCase,
         InvariantCulture,
         InvariantCultureIgnoreCase,
         Ordinal,
         OrdinalIgnoreCase
         }
}

这涉及应如何解释特定字符串的核心。 许多字符串操作(最重要的是 String.CompareString.Equals)现在公开使用 StringComparison 参数的重载。 在所有情况下都显式设置此参数,而不是选择默认的 String.Compare (string strA、string strB) String.Equals (string a、string b) 使代码更清晰、更易于维护,强烈建议这样做。 此外,为这些 API 指定新的 StringComparison.OrdinalStringComparison.OrdinalIgnore 设置的代码可以获得最大的速度和最常见的正确性优势。

接下来将介绍 StringComparison 成员。

序号字符串操作

指定 StringComparsion.OrdinalStringComparsion.OrdinalIgnoreCase 设置表示非语言比较;也就是说,在此处做出比较决策时,将忽略任何自然语言的特征。 API 使用这些设置运行的基本字符串操作决策基于简单的字节比较,而不是在按区域性参数化的大小写或等效表上运行。 在大多数情况下,这最适合字符串的预期解释,同时使代码更快、更可靠。

  • 序号比较 是字符串比较,其中每个字符串的每个字节在没有语言解释的情况下进行比较。 这实质上是一个 C 运行时 strcmp。 因此,“windows”与“Windows”不匹配。如果上下文规定字符串应完全匹配,或要求保守匹配策略,则应使用此比较。 此外,序号比较速度最快,因为它们在确定结果时不应用任何语言规则。
  • 不区分大小写的序号比较 是下一个最保守的比较,并忽略大多数大小写。 因此,“windows”将匹配“Windows”。处理 ASCII 字符时,此策略等效于 StringComparison.Ordinal 的策略,但忽略了通常的 ASCII 大小写。 因此,[AZ] (\u0041-\u005A) 中的任何字符都与 [az] (\u0061-\007A) 中的相应字符匹配。 ASCII 范围外的大小写使用固定区域性的表;因此,调用
String.Compare(strA, strB, StringComparsion.OrdinalIgnoreCase) 

等效于 (,但比) 调用更快

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

这些比较仍非常快。

这两种类型的序号比较都直接使用二进制值的等效性,并且最适合匹配。 如果对比较设置有疑问,请使用这两个值之一。 但是,由于它们按字节比较操作,因此它们排序不是按语言排序顺序 (类似于英语词典) 而是按二进制排序顺序排序,如果大多数上下文中向用户显示,这种排序顺序可能看起来很奇怪。

对于未使用 StringComparsion 参数的 String.Equalsion 重载 (包括==) ,序号语义为默认值。 建议在任何情况下都指定 StringComparison

使用当前区域性的字符串操作

对于语言相关的数据(应在不同区域性之间以不同的方式进行解释),请使用以下操作:

  • CurrentCulture 比较使用线程的当前区域性或“区域设置”;如果未由用户设置,则默认为控制面板“区域选项”窗口中的设置。 这些应用于区分区域性的用户交互。 如果当前区域性设置为美国英语 (“en-US”) ,“visualStudio”将按排序顺序显示在“窗口”之前,就像在美国英语电话簿中一样。 如果是瑞典 (“sv-SE”) ,情况正好相反。
  • 不区分大小写的 CurrentCulture 比较 与上一个比较相同,只不过它们忽略线程的当前区域性规定的大小写。 此行为也可能以排序顺序显示。

使用 CurrentCulture sematics 的比较是不使用 StringComparisonString.Compare 重载的默认值。 建议在任何情况下都指定 StringComparison

使用固定区域性的字符串操作

InvariantCulture 比较使用静态 CultureInfo.InvariantCultureCompareInfo 属性进行比较信息。 此行为在所有系统上都是相同的,并将超出其范围的任何字符转换为它认为是“等效”固定字符的字符。 此策略可用于跨区域性维护一组字符串行为,但通常会提供意外的结果。

不区分大小写 的 InvariantCulture 比较也使用静态 CultureInfo.InvariantCultureCompareInfo 属性作为比较信息。 所转换字符中的任何大小写差异都将被忽略。

InvariantCulture 具有极少数属性,因此可用于比较。

它以与语言相关的方式进行比较,这阻止它保证完全的符号等效性,但并不是在任何区域性中显示的首选。 也许使用 InvariantCulture 进行比较的唯一真正原因之一是为跨文化相同的显示保留有序数据;如果应用程序附带包含用于显示的已排序标识符列表的大型数据文件,则添加到此列表需要插入固定样式排序。

为方法调用选择 StringComparison 成员

比较 .NET 中的字符串时,存在一些缺陷,例如本文后面介绍的 Turkish-I 问题。 但是,可以通过将字符串与有意义的比较语义一起快速消除其中的大部分内容。 对于给定的上下文,比较样式的适当选择通常变得清晰。

表 1 概述了从语义字符串上下文到 StringComparison 枚举成员的映射:

表 1

数据含义 数据行为 相应的 StringComparsion

  • 区分大小写的内部标识符
  • XML 和 HTTP 等标准中的区分大小写的标识符
  • 区分大小写的安全相关设置
字节完全匹配的非语言标识符。 序号
  • 不区分大小写的内部标识符
  • XML 和 HTTP 等标准中的不区分大小写的标识符
  • 文件路径
  • 注册表项/值
  • 环境变量
  • 资源标识符 (句柄名称,例如)
  • 不区分大小写的安全相关设置
非语言标识符,其中大小写不相关,尤其是存储在大多数 Microsoft Windows 系统服务中的一段数据。 OrdinalIgnoreCase
  • 一些持久保存的语言相关数据
  • 显示需要固定排序顺序的语言数据
与文化无关的数据,这在语言上仍然相关。 InvariantCulture

InvariantCultureIgnoreCase

  • 向用户显示的数据
  • 大多数用户输入
需要本地语言自定义的数据。 CurrentCulture

CurrentCultureIgnoreCase

动机:土耳其-我问题

存在这些新建议和 API 可以缓解对默认字符串 API 行为的错误假设。 在语言上解释非语言字符串数据时出现的 bug 的规范示例是“土耳其语-I”问题。

对于几乎所有拉丁字母表(包括美国英语),字符 i (\u0069) 是字符 I (\u0049) 的小写版本。 此大小写规则快速成为在此类区域性中编程的人员的默认设置。 但是,在土耳其语 (“tr-TR”) 中,存在大写字母“i with a dot”,字符 (\u0130) ,这是 i 的大写版本。 同样,在土耳其语中,有一个小写的“i 没有点”,或 (\u0131) ,大写为 I。此行为也发生在 Azeri 区域性 (“az”) 中。

因此,通常关于大写 i 或降低 I 的假设并非在所有区域性中都有效。 如果使用字符串比较例程的默认重载,则它们将受到区域性差异的约束。 对于非语言数据,如以下示例所示,可能会生成不需要的结果:

Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US")
Console.WriteLine("Culture = {0}",
   Thread.CurrentThread.CurrentCulture.DisplayName);
Console.WriteLine("(file == FILE) = {0}", 
   (String.Compare("file", "FILE", true) == 0));

Thread.CurrentThread.CurrentCulture = new CultureInfo("tr-TR");
Console.WriteLine("Culture = {0}",
   Thread.CurrentThread.CurrentCulture.DisplayName);
Console.WriteLine("(file == FILE) = {0}", 
   (String.Compare("file", "FILE", true) == 0));

由于 I 的比较不同,因此当线程区域性发生更改时,比较的结果会发生变化。 以下是输出:

Culture = English (United States)
(file == FILE) = True
Culture = Turkish (Turkey)
(file == FILE) = False

如果在安全敏感设置中无意中使用区域性,这可能会导致实际问题:

static String IsFileURI(String path) {
    return (String.Compare(path, 0, "FILE:", 0, 5, true) == 0);
}

类似于 IsFileURI (“file:”) 将返回 true ,当前区域性为美国英语,但如果区域性为土耳其语,则返回 false 。 因此,在土耳其系统上,人们可能会绕过安全措施,阻止访问以“FILE:”开头的不区分大小写的 URI。 由于“file:”旨在解释为非语言不区分区域性的标识符,因此应按以下方式编写代码:

static String IsFileURI(String path) {
    return (String.Compare(path, 0, "FILE:", 0, 5,
      StringComparison.OrdinalIgnoreCase) == 0);
}

原始的土耳其-I 解决方案及其不足

由于 Turkish-I 问题,.NET 团队最初建议使用 InvariantCulture 作为主要的跨区域性比较类型。 然后,前面的代码将如下所示:

static String IsFileURI(String path) {
   return (String.Compare(path, 0, "FILE:", 0, 5, true,
      CultureInfo.InvariantCulture) == 0);
}

在 ASCII 字符串上使用 InvariantCultureOrdinal 的比较将具有相同的效果;但是, InvariantCulture 将做出可能不适合需要解释为一组字节的字符串的语言决策。

使用 CultureInfo.InvariantCulture.CompareInfo,某些字符集在 Compare () 下等效。 例如,以下等效性位于固定区域性下:

InvariantCulture:a + =å

“拉丁文小写字母 a” (\u0061) 字符 a,在“上方组合环” (\u030a) 字符 旁边时,将被解释为“带上环的拉丁文小写字母 a” (\u00e5) 字符 å。

示例 3:

string separated = "\u0061\u030a";
string combined = "\u00e5";
      
Console.WriteLine("Equal sort weight under InvariantCulture? {0}",
   String.Compare(separated, combined, 
      StringComparison.InvariantCulture) == 0);
         

Console.WriteLine("Equal sort weight under 
   Ordinal? {0}",
   String.Compare(separated, combined, 
      StringComparison.Ordinal) == 0);

这会打印出:

Equal sort weight under InvariantCulture? True
Equal sort weight under Ordinal? False

因此,在解释文件名、Cookie 或其他任何可能出现类似 å 组合的内容时,序号比较仍然提供最透明和合适的行为。

框架中的常见字符串比较方法

以下方法集是最常用于字符串比较的方法,并附有说明供其使用。

String.Compare

默认解释:CurrentCulture

作为字符串解释最核心的操作,应根据当前区域性检查这些方法调用的所有实例来确定是否应该从区域性(符号)解释或分离字符串。 通常,它是后者,应使用序号比较。

作为所有 System.Globalization.CultureInfo 对象的 CompareInfo 属性提供的 System.Globalization.CompareInfo 类还提供 Compare 方法,该方法通过 CompareOptions 标志枚举提供大量匹配选项 (序号、忽略空格、忽略假名类型) 等。

String.CompareTo

默认解释:CurrentCulture

此 API 当前不提供指定 StringComparison 类型的重载。 通常可以将这些方法转换为建议的 String.Compare (string、string、StringComparison) 形式。

实现 IComparable 接口必须使用此方法。 由于它不提供 StringComparison 参数的选项,因此实现此功能的类型通常允许用户在其构造函数中指定 StringComparer , (请参阅以下哈希表示例) 。

String.Equals

默认解释:序号

String 类的相等方法包括静态 Equals、静态运算符 ==和实例方法 Equals。 默认情况下,所有这些操作都以序号方式运行。 即使需要序号比较,仍建议使用显式声明 StringComparison 类型的重载;这样,在代码中搜索特定字符串解释变得更加容易。

String.ToUpper 和 String.ToLower

默认解释:CurrentCulture

使用这些函数时,用户当然应该小心,因为强制字符串到特定大小写通常用作比较字符串的小型规范化,而不考虑大小写。 如果是这样,请考虑使用不区分大小写的比较。

ToUpperInvariantToLowerInvariant 也可用。 ToUpperInvariant 是规范化大小写的标准方式。 使用 OrdinalIgnoreCase 进行的比较在行为上由两个调用组成:对两个字符串参数调用 ToUpperInvariant ,以及执行 序号 比较。

在给定 CultureInfo 参数的情况下,重载也可用于转换为大写和小写。

Char.ToUpper 和 Char.ToLower

默认解释:CurrentCulture

这些方法的工作方式类似于前面所述的可比较的 String 方法。

String.StartsWith 和 String.EndsWith

StartsWith (字符串) 的默认解释:CurrentCulture

这些方法的新重载使用 StringComparison 类型。

String.IndexOf 和 String.LastIndexOf

IndexOf (字符串, ...) 默认解释:CurrentCulture

IndexOf (char, ...) 默认解释:序号

在编写 (.NET 2.0 Beta 2) 之后,这些当前不会使用 StringComparison 公开重载。 他们将随 .NET 2.0 的完整版本一起提供。 还可以考虑使用 System.Globalization.CompareInfo 类公开的重载。

用于正确字符串比较的新 Whidbey API 列表

下面重申了一组新的 Whidbey API,这些 API 直接使用户能够显式指定字符串比较类型。

public class String
{
   bool Equals(String value, StringComparison comparisonType)
   static bool Equals(String a, String b, StringComparison comparisonType)    

   static int Compare(String strA, String strB, StringComparison 
      comparisonType)
   static int Compare(String strA, int indexA, String strB, int indexB, int 
      length, StringComparison comparisonType)

   bool StartsWith(String value, StringComparison comparisonType)
   bool StartsWith(String value, bool ignoreCase, CultureInfo culture)

   bool EndsWith(String value, StringComparison comparisonType)
   bool EndsWith(String value, bool ignoreCase, CultureInfo culture)

   string ToLowerInvariant()
   string ToUpperInvariant()
}

public abstract class StringComparer : IComparer, IEqualityComparer,
IComparer<string>, IEqualityComparer<string>
{      

   public static StringComparer InvariantCulture        
   public static StringComparer InvariantCultureIgnoreCase
   public static StringComparer CurrentCulture 
   public static StringComparer CurrentCultureIgnoreCase
   public static StringComparer Ordinal
   public static StringComparer OrdinalIgnoreCase
   public static StringComparer Create(CultureInfo culture,
         bool ignoreCase)
   public int Compare(object x, object y)
   public new bool Equals(Object x, Object y)
   public int GetHashCode(object obj) 
   public abstract int Compare(String x, String y);
   public abstract bool Equals(String x, String y);        
   public abstract int GetHashCode(string obj);     
 }

受二次影响 API 的示例:System.Collections

某些将字符串比较作为中心操作的非字符串 API 使用新的 StringComparer 类型。 此类型以明确的方式封装 StringComparsion 枚举提供的比较。

Array.Sort 和 Array.BinarySearch

默认解释:CurrentCulture

如果将任何数据存储到集合,或者将持久化数据从文件或数据库读取到集合,请注意,切换区域性可能会使集合中固有的不变量失效。 Array.BinarySearch 假定基础内容已排序;如果内容是字符串,则 Array.Sort 将使用此排序使用 String.Compare 。 使用区分区域性的比较器可能会很危险,以防区域性在排序数组和搜索数组内容之间发生更改。

示例 4 (不正确的) :

string []storedNames;

public void StoreNames(string [] names)
{
   int index = 0;
   storedNames = new string[names.Length];

   foreach (name in names)
   {
      this.storedNames[index++] = name;
   }

   Array.Sort(names); // line A
}

public bool DoesNameExist(string [] names)
{
   return (Array.BinarySearch(this.storedNames) >= 0); // line B
}

此处的存储和检索操作于 Thread.CurrentCulture 提供的比较器。 如果预计在对 StoreNames 和 DoesNameExist 的调用之间发生区域性更改,尤其是当内容保存在两者之间的某个位置时,二进制搜索可能会失败。

以下代码中显示了粗体行的正确替换。

示例 4 (正确) :

Array.Sort(names, StringComparer.Ordinal); // line A
// ...
Array.BinarySearch(names, StringComparer.Ordinal); // line B

如果此数据在区域性之间持久保存和移动,并且使用排序来向用户呈现此数据,则人们甚至可能会考虑很少使用 InvariantCulture,它以语言方式运行,以提高用户输出,但不受区域性更改影响:

Array.Sort(names, StringComparer.InvariantCulture); // lineA
// ...
Array.BinarySearch(names, StringComparer.InvariantCulture); // line B

集合示例:Hashtable 构造函数

哈希字符串是操作的次要示例,在核心上受字符串比较解释的影响。

还应注意,文件系统、注册表和环境变量的字符串行为最好由 OrdinalIgnoreCase 表示。 以下示例演示了如何使用新的抽象 StringComparer 类的成员,该类总结了用于传递到公开 IComparer 参数的现有 API 的比较信息。

const int initialTableCapacity = 100;

public void PopulateFileTable(string directory)
{
   Hashtable h = new Hashtable(initialTableCapacity, 
      StringComparer.OrdinalIgnoreCase);
         
   foreach (string file in Directory.GetFiles(directory))
         h.Add(file, File.GetCreationTime(file));
}

public void PrintCreationTime(string targetFile)
{
   Object dt = h[targetFile];
   if (dt != null)
   {
      Console.WriteLine("File {0} was created at time {1}.",
         targetFile, 
         (DateTime) dt);
   }
   else
   {
      Console.WriteLine("File {0} does not exist.", targetFile);
   }
}

本机代码说明

本机代码容易受到类似类型的错误的影响,但它们的发生要少得多。 字符串操作的默认行为不基于区域设置,但通常基于序号 (strcmpwcscmp,例如) 。 有关使用托管代码的建议镜像此行为。 最后,在需要语言灵活性的情况下,通常可以通过 (请参阅 CompareString) 传递区域性参数。

关于固定区域性的早期建议呢?

使用 InvariantCulture 进行的比较是以前建议的标准,以避免区分区域性的 bug。 序号比较的运行方式与 InvariantCulture 的工作方式相同,而不考虑区域性;但是,它们还有一个额外的好处,即使用 InvariantCulture 进行的隐式语言转换(通常被开发人员忽略)都不会影响比较结果。

建议将编码为即将推出的 FxCop 规则,即:

  • 所有字符串比较操作都使用提供的指定字符串比较类型的重载。
  • 前面所述的重载中 InvariantCulture 的所有用户都强烈考虑使用 OrdinalOrdinalIgnoreCase
  • 如果用于字符串规范化,则应避免对 ToLowerInvariant 的所有调用。

结论

字符串比较和大小写是字符串条件操作的核心,包括排序和相等。 仔细考虑字符串的比较和大小写上下文是使应用程序更快、更正确的最佳方式之一。 选择字符串是应被视为一组符号字节, (序号解释) 还是应因区域性而异 (区分区域性的解释) 在 .NET 2.0 中的新 API 中变得更加清晰。 用户应注意指定正确的解释。 此外,使用序号字符串解释通常是确保代码按预期运行的最佳方式。