字符串操作:模式匹配、性能和基于范围的搜索

本文介绍三个字符串操作:正则表达式模式匹配、 System.Text.RegularExpressions.Regex无分配搜索 ReadOnlySpan<T>以及选择一个 StringComparison 用于正确、快速比较的值。

使用正则表达式查找特定文本

System.Text.RegularExpressions.Regex 在字符串中搜索模式,而不是固定子字符串。 静态 Regex.IsMatch 方法采用输入字符串、模式和可选 RegexOptions 标志。

下面的示例搜索每个句子中不区分大小写的单词。 模式the(ir)?\sthe匹配(可选)后跟ir空格字符:

图案 Meaning
the 匹配文本文本 the
(ir)? 匹配 0 或 1 个匹配项 ir
\s 匹配空格字符
string[] sentences =
[
    "Put the water over there.",
    "They're quite thirsty.",
    "Their water bottles broke."
];

string pattern = @"the(ir)?\s";

foreach (string s in sentences)
{
    Console.Write($"{s,28}");

    if (Regex.IsMatch(s, pattern, RegexOptions.IgnoreCase))
    {
        Console.WriteLine($"  (match for '{pattern}' found)");
    }
    else
    {
        Console.WriteLine();
    }
}

根据模式验证字符串

若要检查整个输入是否与形状匹配,请为图案定位并^$标记 。 以下示例验证每个字符串是否为美国样式的电话号码:三位数字、三位数字、四位数字,用短划线分隔:

图案 Meaning
^ 匹配字符串的开头
\d{3} 完全匹配三位数字符
- 匹配文本 - 字符
\d{4} 完全匹配四位数字符
$ 匹配字符串的末尾
string[] numbers =
[
    "123-555-0190",
    "444-234-22450",
    "690-555-0178",
    "146-893-232",
    "146-555-0122",
    "4007-555-0111",
    "407-555-0111",
    "407-2-5555",
    "407-555-8974",
    "407-2ab-5555",
    "690-555-8148",
    "146-893-232-"
];

string pattern = """^\d{3}-\d{3}-\d{4}$""";

foreach (string s in numbers)
{
    Console.Write($"{s,14}");
    Console.WriteLine(Regex.IsMatch(s, pattern) ? " - valid" : " - invalid");
}

有关完整模式语法,请参阅 正则表达式语言 - 快速参考

在方法和正则表达式之间进行 string 选择

string 方法和 Regex 解决重叠问题。 在 string 搜索的文本是文本值、已知前缀或后缀或固定分隔符时首选方法。 它们更易于阅读和更快地阅读,因为它们不支付编译和执行模式的费用。 Regex当搜索目标为形状(例如交替、可选组、重复字符类或定位验证)时进行访问。 作为经验法则,如果可以将搜索编写为一两string.Contains / / StartsWithIndexOf个调用,请执行此操作。

使用 ReadOnlySpan<char>

分析大型输入或在热路径上运行搜索时,每个调用分配 string.Substring 可以 string.Split 占据主导地位。 ReadOnlySpan<char> 提供对现有字符串(或数组或堆栈缓冲区)的视图,而无需复制,并提供 MemoryExtensions 通用 string 方法的基于范围的等效项,包括 IndexOf

ReadOnlySpan<char> input = "key1=alpha;key2=beta;key3=gamma".AsSpan();
ReadOnlySpan<char> needle = "key2=".AsSpan();

int start = input.IndexOf(needle);
if (start >= 0)
{
    ReadOnlySpan<char> rest = input[(start + needle.Length)..];
    int end = rest.IndexOf(';');
    ReadOnlySpan<char> value = end >= 0 ? rest[..end] : rest;
    Console.WriteLine($"key2 = {value}");
}
// => key2 = beta

基于范围的搜索避免分配,因为切片 (input[start..]rest[..end]) 只是原始字符的窗口。 相同的方法可缩放为分析键值列表、标头和其他带分隔符的文本,而无需调用 Substring

StringComparison 的性能注意事项

大多数 string 实例方法都有接受值的 StringComparison 重载。 String.Equals(String)默认为序号,但String.Compare(String, String)String.IndexOf(String)默认为当前区域性的方法。 这种差异有两种方式:

  • 速度。 序号比较是在紧密矢量化循环中运行的字节 for-byte 测试。 区域性感知比较会查阅排序表、演练组合字符并应用特定于区域设置的规则。 对于同一输入,其数量级可能变慢。
  • 正确性。 文化感知比较可以折叠你不希望的字符(土耳其 i/I,德语 ßss,连字)。 此行为适合对用户看到的名称进行排序,但分析标识符、路径或协议令牌时出错。

对于计算机定义的文本,例如文件名、URL、HTTP 标头、标识符和配置密钥,请传递或显式传递或StringComparison.OrdinalIgnoreCase显式传递StringComparison.Ordinal。 为向用户显示的自然语言文本保留区域性感知值。 有关全面指导,请参阅 比较 .NET 中的字符串的最佳做法。

另请参阅