如何:比较 C# 中的字符串
通过比较字符串可以回答两个问题,一个是:“这两个字符串相等吗?”另一个是“排序时,应该按什么顺序排列这些字符串?”
这两个问题非常复杂,因为字符串比较受很多因素的影响:
- 可以选择序号比较或语义比较。
- 可以选择是否区分大小写。
- 可以选择区域性特定的比较。
- 语义比较取决于区域性和平台。
System.StringComparison 枚举字段代表以下选项:
- CurrentCulture:使用区分区域性的排序规则和当前区域性比较字符串。
- CurrentCultureIgnoreCase:通过使用区分区域性的排序规则、当前区域性,并忽略所比较的字符串的大小写,来比较字符串。
- InvariantCulture:使用区分区域性的排序规则和固定区域性比较字符串。
- InvariantCultureIgnoreCase:通过使用区分区域性的排序规则、固定区域性,并忽略所比较的字符串的大小写,来比较字符串。
- Ordinal:使用序号(二进制)排序规则比较字符串。
- OrdinalIgnoreCase:通过使用序号(二进制)区分区域性的排序规则并忽略所比较的字符串的大小写,来比较字符串。
注意
本文中的 C# 示例运行在 Try.NET 内联代码运行程序和演练环境中。 选择“运行”按钮以在交互窗口中运行示例。 执行代码后,可通过再次选择“运行”来修改它并运行已修改的代码。 已修改的代码要么在交互窗口中运行,要么编译失败时,交互窗口将显示所有 C# 编译器错误消息。
在比较字符串时定义它们的顺序。 通过比较决定字符串的序列。 确定序列顺序后,软件和人工都可以更轻松地进行搜索。 其他比较可能会检查字符串是否相同。 这种一致性检查与相等性检查类似,但是也有一些不同之处,例如可能会忽略大小写的差异。
默认的序号比较
默认情况下,最常见的操作:
- String.Equals
- String.Equality 和 String.Inequality,即相等运算符
==
和!=
,分别执行区分大小写的序号比较。 String.Equals 具有重载,可以提供 StringComparison 参数来更改其排序规则。 下面的示例演示了这一操作:
string root = @"C:\users";
string root2 = @"C:\Users";
bool result = root.Equals(root2);
Console.WriteLine($"Ordinal comparison: <{root}> and <{root2}> are {(result ? "equal." : "not equal.")}");
result = root.Equals(root2, StringComparison.Ordinal);
Console.WriteLine($"Ordinal comparison: <{root}> and <{root2}> are {(result ? "equal." : "not equal.")}");
Console.WriteLine($"Using == says that <{root}> and <{root2}> are {(root == root2 ? "equal" : "not equal")}");
默认序号比较在比较字符串时不会考虑语义规则。 它会比较两个字符串中每个 Char 对象的二进制值。 因此,默认的序号比较也是区分大小写的。
使用 String.Equals 以及 ==
和 !=
运算符的相等性测试不同于使用 String.CompareTo 和 Compare(String, String) 方法的字符串比较。 它们均执行区分大小写的比较。 但是,虽然相等性测试执行顺序比较,但 CompareTo
和 Compare
方法使用当前区域性执行区域性感知的语言比较。 调用显式指定要执行的比较类型的重载,确保代码意图明确。
不区分大小写的序号比较
采用 String.Equals(String, StringComparison) 方法可指定 StringComparison.OrdinalIgnoreCase 的 StringComparison 值来进行不区分大小写的序号比较。 此外还有静态 String.Compare(String, String, StringComparison) 方法,该方法执行不区分大小写的序号比较,前提是你指定 StringComparison 参数的 StringComparison.OrdinalIgnoreCase 值。 在下面的代码中演示了这些比较:
string root = @"C:\users";
string root2 = @"C:\Users";
bool result = root.Equals(root2, StringComparison.OrdinalIgnoreCase);
bool areEqual = String.Equals(root, root2, StringComparison.OrdinalIgnoreCase);
int comparison = String.Compare(root, root2, comparisonType: StringComparison.OrdinalIgnoreCase);
Console.WriteLine($"Ordinal ignore case: <{root}> and <{root2}> are {(result ? "equal." : "not equal.")}");
Console.WriteLine($"Ordinal static ignore case: <{root}> and <{root2}> are {(areEqual ? "equal." : "not equal.")}");
if (comparison < 0)
Console.WriteLine($"<{root}> is less than <{root2}>");
else if (comparison > 0)
Console.WriteLine($"<{root}> is greater than <{root2}>");
else
Console.WriteLine($"<{root}> and <{root2}> are equivalent in order");
执行不区分大小写的序号比较时,这些方法使用固定区域性的大小写约定。
语义比较
许多字符串比较方法(例如 String.StartsWith)默认使用当前区域性的语言规则对其输入进行排序。 这种语言比较有时被称为“文字排序顺序”。在执行语义比较时,一些非字母数字的 Unicode 字符可能分配有特殊的权重。 例如,连字符“-”分配的权重可能很小,所以“co-op”和“coop”在排序顺序中会彼此相邻。 一些非打印控制字符可能会被忽略。 此外,一些 Unicode 字符可能与一系列 Char 实例等效。 下面以德语短句“他们在街上跳舞。”为例;其中,一个字符串中有“ss”(U+0073 U+0073),另一个字符串中有“ß”(U+00DF)。 (在 Windows 系统中)从语义上说,“ss”在“en-US”和“de-DE”区域性中都等同于德语中的“'ß”。
string first = "Sie tanzen auf der Straße.";
string second = "Sie tanzen auf der Strasse.";
Console.WriteLine($"First sentence is <{first}>");
Console.WriteLine($"Second sentence is <{second}>");
bool equal = String.Equals(first, second, StringComparison.InvariantCulture);
Console.WriteLine($"The two strings {(equal == true ? "are" : "are not")} equal.");
showComparison(first, second);
string word = "coop";
string words = "co-op";
string other = "cop";
showComparison(word, words);
showComparison(word, other);
showComparison(words, other);
void showComparison(string one, string two)
{
int compareLinguistic = String.Compare(one, two, StringComparison.InvariantCulture);
int compareOrdinal = String.Compare(one, two, StringComparison.Ordinal);
if (compareLinguistic < 0)
Console.WriteLine($"<{one}> is less than <{two}> using invariant culture");
else if (compareLinguistic > 0)
Console.WriteLine($"<{one}> is greater than <{two}> using invariant culture");
else
Console.WriteLine($"<{one}> and <{two}> are equivalent in order using invariant culture");
if (compareOrdinal < 0)
Console.WriteLine($"<{one}> is less than <{two}> using ordinal comparison");
else if (compareOrdinal > 0)
Console.WriteLine($"<{one}> is greater than <{two}> using ordinal comparison");
else
Console.WriteLine($"<{one}> and <{two}> are equivalent in order using ordinal comparison");
}
在 Windows 上,.NET 5 之前,从语义比较改为序号比较时,“cop”、“coop”和“co-op”的排序顺序产生了变化。 使用不同的比较类型时,这两个德语句子的比较结果也就不同了。 在 .NET 5 之前,.NET 全球化 API 使用国家语言支持 (NLS) 库。 在 .NET 5 和更高版本中,.NET 全球化 API 使用 International Components for Unicode (ICU) 库,该库统一所有支持的操作系统中的 .NET 全球化行为。
使用特定区域性的比较
以下示例为 en-US 和 de-DE 区域性存储 CultureInfo 对象。 使用 CultureInfo 对象执行比较以确保执行的是特定于区域性的比较。 所用的区域性会对语义比较产生影响。 以下示例展示使用“en-US”区域性和“de-DE”区域性对两个德语句子进行比较的结果:
string first = "Sie tanzen auf der Straße.";
string second = "Sie tanzen auf der Strasse.";
Console.WriteLine($"First sentence is <{first}>");
Console.WriteLine($"Second sentence is <{second}>");
var en = new System.Globalization.CultureInfo("en-US");
// For culture-sensitive comparisons, use the String.Compare
// overload that takes a StringComparison value.
int i = String.Compare(first, second, en, System.Globalization.CompareOptions.None);
Console.WriteLine($"Comparing in {en.Name} returns {i}.");
var de = new System.Globalization.CultureInfo("de-DE");
i = String.Compare(first, second, de, System.Globalization.CompareOptions.None);
Console.WriteLine($"Comparing in {de.Name} returns {i}.");
bool b = String.Equals(first, second, StringComparison.CurrentCulture);
Console.WriteLine($"The two strings {(b ? "are" : "are not")} equal.");
string word = "coop";
string words = "co-op";
string other = "cop";
showComparison(word, words, en);
showComparison(word, other, en);
showComparison(words, other, en);
void showComparison(string one, string two, System.Globalization.CultureInfo culture)
{
int compareLinguistic = String.Compare(one, two, en, System.Globalization.CompareOptions.None);
int compareOrdinal = String.Compare(one, two, StringComparison.Ordinal);
if (compareLinguistic < 0)
Console.WriteLine($"<{one}> is less than <{two}> using en-US culture");
else if (compareLinguistic > 0)
Console.WriteLine($"<{one}> is greater than <{two}> using en-US culture");
else
Console.WriteLine($"<{one}> and <{two}> are equivalent in order using en-US culture");
if (compareOrdinal < 0)
Console.WriteLine($"<{one}> is less than <{two}> using ordinal comparison");
else if (compareOrdinal > 0)
Console.WriteLine($"<{one}> is greater than <{two}> using ordinal comparison");
else
Console.WriteLine($"<{one}> and <{two}> are equivalent in order using ordinal comparison");
}
区分区域性的比较通常用于对用户输入的字符串以及用户输入的其他字符串进行比较和排序。 字符和这些字符的排序约定可能会根据用户计算机的区域设置而有所不同。 即使是包含相同字符的字符串,也可能因当前线程的区域性而具有不同的排序。
数组中的语义排序和字符串搜索
以下示例演示如何在数组中使用依赖当前区域性的语义比较对字符串进行排序和搜索。 使用采用 System.StringComparer 参数的静态 Array 方法。
以下示例演示如何使用当前区域性对字符串数组进行排序:
string[] lines = new string[]
{
@"c:\public\textfile.txt",
@"c:\public\textFile.TXT",
@"c:\public\Text.txt",
@"c:\public\testfile2.txt"
};
Console.WriteLine("Non-sorted order:");
foreach (string s in lines)
{
Console.WriteLine($" {s}");
}
Console.WriteLine("\n\rSorted order:");
// Specify Ordinal to demonstrate the different behavior.
Array.Sort(lines, StringComparer.CurrentCulture);
foreach (string s in lines)
{
Console.WriteLine($" {s}");
}
对数组进行排序后,可以使用二分搜索法搜索条目。 二分搜索法从集合的中间开始搜索,判断集合的哪一半包含所找字符串。 后续的每个比较都将集合的剩余部分再次对半分开。 使用 StringComparer.CurrentCulture 存储数组。 本地函数 ShowWhere
显示发现字符串所在位置的信息。 如果未找到字符串,返回的值会指示可以在其中找到字符串的位置。
string[] lines = new string[]
{
@"c:\public\textfile.txt",
@"c:\public\textFile.TXT",
@"c:\public\Text.txt",
@"c:\public\testfile2.txt"
};
Array.Sort(lines, StringComparer.CurrentCulture);
string searchString = @"c:\public\TEXTFILE.TXT";
Console.WriteLine($"Binary search for <{searchString}>");
int result = Array.BinarySearch(lines, searchString, StringComparer.CurrentCulture);
ShowWhere<string>(lines, result);
Console.WriteLine($"{(result > 0 ? "Found" : "Did not find")} {searchString}");
void ShowWhere<T>(T[] array, int index)
{
if (index < 0)
{
index = ~index;
Console.Write("Not found. Sorts between: ");
if (index == 0)
Console.Write("beginning of sequence and ");
else
Console.Write($"{array[index - 1]} and ");
if (index == array.Length)
Console.WriteLine("end of sequence.");
else
Console.WriteLine($"{array[index]}.");
}
else
{
Console.WriteLine($"Found at index {index}.");
}
}
集合中的序号排序和搜索
以下代码使用 System.Collections.Generic.List<T> 集合类存储字符串。 字符串是通过 List<T>.Sort 方法排序的。 此方法需要对两个字符串进行比较和排序的委托。 String.CompareTo 方法提供该比较函数。 请运行示例并观察顺序。 此排序操作使用区分大小写的序号排序。 你要使用静态 String.Compare 方法指定不同的比较规则。
List<string> lines = new List<string>
{
@"c:\public\textfile.txt",
@"c:\public\textFile.TXT",
@"c:\public\Text.txt",
@"c:\public\testfile2.txt"
};
Console.WriteLine("Non-sorted order:");
foreach (string s in lines)
{
Console.WriteLine($" {s}");
}
Console.WriteLine("\n\rSorted order:");
lines.Sort((left, right) => left.CompareTo(right));
foreach (string s in lines)
{
Console.WriteLine($" {s}");
}
排序后,可以使用二分搜索法对字符串列表进行搜索。 以下示例演示如何使用相同的比较函数搜索排序列表。 本地函数 ShowWhere
显示所查找的文本所在的位置或可能会在位置:
List<string> lines = new List<string>
{
@"c:\public\textfile.txt",
@"c:\public\textFile.TXT",
@"c:\public\Text.txt",
@"c:\public\testfile2.txt"
};
lines.Sort((left, right) => left.CompareTo(right));
string searchString = @"c:\public\TEXTFILE.TXT";
Console.WriteLine($"Binary search for <{searchString}>");
int result = lines.BinarySearch(searchString);
ShowWhere<string>(lines, result);
Console.WriteLine($"{(result > 0 ? "Found" : "Did not find")} {searchString}");
void ShowWhere<T>(IList<T> collection, int index)
{
if (index < 0)
{
index = ~index;
Console.Write("Not found. Sorts between: ");
if (index == 0)
Console.Write("beginning of sequence and ");
else
Console.Write($"{collection[index - 1]} and ");
if (index == collection.Count)
Console.WriteLine("end of sequence.");
else
Console.WriteLine($"{collection[index]}.");
}
else
{
Console.WriteLine($"Found at index {index}.");
}
}
在排序和搜索过程中,请始终确保使用相同的比较类型。 使用不同的比较类型进行排序和搜索会产生意外的结果。
元素或键的类型为 string
时,System.Collections.Hashtable、System.Collections.Generic.Dictionary<TKey,TValue> 和 System.Collections.Generic.List<T> 等集合类的构造函数具有 System.StringComparer 参数。 通常,应尽可能使用这些构造函数,并指定 StringComparer.Ordinal 或 StringComparer.OrdinalIgnoreCase。