正则表达式(regex)是一种描述文本的模式:“四个数字、一个短横线、两个数字”,“以大写字母开头的单词”,“方括号之间的任何内容”。在 C# 中,System.Text.RegularExpressions 中的 Regex 类可以用这样的模式来查找、提取、替换和分割文本。
IsMatch、Match 与 Matches
三个方法就能覆盖大多数搜索。IsMatch 回答是或否,Match 返回第一个匹配,Matches 返回所有匹配:
输出:
True
2026-03-14 at index 22
4 matches:
1042
2026
1043
2026
Success: False, Value: []
匹配失败的 Match 不会返回 null:它返回一个 Success 为 false、Value 为空的 Match。使用匹配结果之前,一定要先检查 Success。
查找四位数字的例子说明了模式会匹配到比预期更多的内容。\b\d{4}\b 的意思是“作为一个完整单词的恰好四个数字”,而日期里的年份也符合条件,因为 - 是一个单词边界。要把模式写得更精确,表达你真正的意思,例如 (?<=Order |order )\d+,或者使用下一节介绍的分组。
用逐字字符串写模式
正则语法到处都是反斜杠(\d、\s、\b),C# 字符串的转义也是。在普通字符串中,"\d" 甚至无法编译(CS1009,无法识别的转义序列),而 "\b" 会被编译成一个退格符,悄悄改变了模式。请用带 @ 的逐字字符串来写模式,在其中反斜杠就只是反斜杠:
var a = new Regex("\\d+\\.\\d{2}"); // escaped twice: hard to read
var b = new Regex(@"\d+\.\d{2}"); // verbatim: what the regex engine sees
在逐字字符串中,双引号要写成 ""。C# 11 的原始字符串字面量("""...""")连这一点都可以避免。
分组:提取匹配的各个部分
括号创建一个分组,匹配之后可以拿到每个分组的文本。分组按左括号出现的顺序从 1 开始编号;第 0 组是整个匹配。命名分组 (?<name>...) 更容易阅读,修改模式时也不会受影响:
输出:
2026-03-14
ERROR
payments
Card declined for order 1042
60 by 90, area 5400
[ 和 ] 在正则中有特殊含义(字符类),所以要匹配字面上的方括号,需要写 \[ 和 \]。. * + ? ( ) { } ^ $ | \ 也是如此。要在模式中按字面匹配用户提供的字符串,就先把它交给 Regex.Escape,它会帮你转义这些元字符:Regex.Escape("price (USD)") 返回 price\ \(USD\)(空格也会被转义,这没有坏处)。
只用来分组、不捕获的括号写作 (?:...)。它们能让分组编号保持整洁,速度也稍快一些。
Replace:替换模式与 lambda
Regex.Replace 会替换每一个匹配。在替换字符串中,$1 插入第 1 组,${name} 插入命名分组,$0 插入整个匹配。对于模板表达不了的情况,可以传入一个函数(通常是 lambda),它接收每个 Match 并返回对应的替换内容:
输出:
Invoiced 14/03/2026, paid 02/04/2026.
too many spaces
Card **** **** **** 1234
Tea 2.75, Scone 3.52, Jam 1.10
银行卡号的掩码用到了先行断言 (?=...):只有后面还跟着四个数字时,它才匹配当前这个数字,而且不会消耗后面的数字。环视((?=...)、(?!...)、(?<=...)、(?<!...))只检查上下文,不把上下文包含在匹配中,正因如此,一次 Replace 就能完整保留最后四位数字。
对于没有模式的普通文本,string.Replace 更简单也更快;当要替换的内容不固定时,再用 Regex.Replace。
Split
Regex.Split 在模式的每个匹配处进行分割,可以处理不固定的分隔符:
输出:
csharp / dotnet / regex / tutorial / beginner
Order Shipped Event Handler
如果模式中包含捕获分组,Regex.Split 会把捕获到的分隔符也放进结果里;不需要它们时,请使用 (?:...)。
RegexOptions
选项会改变整个模式的行为。用 | 组合它们:
输出:
1
2
error INFO Error
True
你会用到的选项:
IgnoreCase:不区分大小写的匹配(内联形式为(?i))。Multiline:^和$匹配每一行的开头和结尾,而不只是整个字符串的开头和结尾。Singleline:.也能匹配\n(默认情况下它匹配除换行符以外的任何字符)。IgnorePatternWhitespace:忽略模式中的空格,#开始一段注释,适合分多行书写的长模式。CultureInvariant:与IgnoreCase一起使用时,比较不受当前区域文化规则的影响。Compiled:把模式一次性编译为 IL,适合要使用很多次的正则表达式。
校验输入:锚点与邮箱问题
用于校验时,要用 ^ 和 $ 给模式加上锚点,否则只要输入的一部分匹配,校验就会通过:
输出:
PT-1000 loose=True strict=True
pt-1000 loose=False strict=False
XPT-1000Y loose=True strict=False
PT-10 loose=False strict=False
ana@example.com True
ana@example False
ana @example.com False
ana@@example.com False
这个邮箱模式只检查格式:一段不含空格和 @ 的内容,一个 @,以及一个带点的域名。这是有意为之的。邮箱地址真正的语法(RFC 5322)允许带引号的本地部分、注释以及 IP 字面量形式的域名;试图覆盖这些的模式会长达几百个字符,却仍然会拒绝人们实际在用的合法地址。而且语法完全正确的地址也可能退信。先检查格式,再发送一封确认邮件。如果你不想自己写模式,System.Net.Mail.MailAddress 也提供了另一种格式检查。
性能:静态方法、实例与编译
静态方法(Regex.IsMatch(input, pattern))会解析模式,并把它保存在一个小缓存中(默认 15 个模式),所以用同一个模式重复调用的开销很小。对于在热点循环中使用的模式,创建一个 Regex 实例并保存在 static readonly 字段中;如果它要运行成千上万次,再加上 RegexOptions.Compiled,用更慢的启动换取更快的匹配。
超时与灾难性回溯
有些模式在特定输入下需要指数级的时间。经典的例子是 ^(a+)+$ 这种嵌套量词去匹配 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!":引擎会尝试把这些 a 分配给两个 + 的每一种方式,最后才放弃。在 Web 服务器上,用户发来的一个这样的输入就能让一个 CPU 核心一直满载(即“ReDoS”攻击)。凡是处理不可信输入的正则表达式,都要设置超时:
var pattern = new Regex(@"^(\w+\s?)*$", RegexOptions.None, TimeSpan.FromMilliseconds(100));
try
{
bool ok = pattern.IsMatch(userInput);
}
catch (RegexMatchTimeoutException)
{
// treat as invalid input
}
每个静态方法也都有一个接收超时参数的重载。.NET 7 还新增了 RegexOptions.NonBacktracking,这个引擎保证线性时间,代价是失去一些功能(环视、反向引用)。
GeneratedRegex(.NET 7)
.NET 7 新增了一个源代码生成器,它在编译时把模式转换成普通的 C# 代码。你能获得 Compiled 的速度,却没有启动开销;生成的代码可以在 IDE 中阅读,而且支持裁剪和 Native AOT:
public static partial class Patterns
{
[GeneratedRegex(@"^[A-Z]{2}-\d{4}$", RegexOptions.IgnoreCase)]
public static partial Regex ProductCode();
}
bool ok = Patterns.ProductCode().IsMatch("pt-1000"); // True
在 .NET 7 及以后版本中,对于编译时就已知的模式,推荐使用这种写法。
常见错误
- 校验时缺少锚点。 没有
^...$时,只要输入中包含一个匹配,IsMatch就会接受它。 - 用普通字符串写模式。
"\b"是退格符,而不是单词边界。请使用@"..."。 - 特殊字符没有转义。
.匹配任何字符;把3.50当作模式时,它也会匹配3x50。用\.转义,用户输入则用Regex.Escape转义。 - 贪婪量词。 用
<.*>匹配<b>bold</b>会匹配整个字符串。请使用惰性的.*?或否定字符类[^>]*。 - 处理不可信输入时不设超时。 一个嵌套量词就可能让请求卡死。
- 用正则表达式处理结构化格式。 带引号的 HTML、JSON 和 CSV 需要的是解析器,而不是模式。
常见问题
C# 中如何使用正则表达式?
添加 using System.Text.RegularExpressions;,然后调用 Regex 上的静态方法:Regex.IsMatch(input, pattern) 返回一个 bool,Regex.Match 返回第一个匹配,Regex.Matches 返回所有匹配,Regex.Replace 进行替换,Regex.Split 进行分割。把模式写成逐字字符串 @"\d+",这样反斜杠就能原样交给正则引擎。
C# 中如何从正则匹配结果中获取分组的值?
用括号把你想要的部分括起来,然后读取 match.Groups[1].Value(分组从 1 开始编号;第 0 组是整个匹配)。命名分组更清晰:(?<year>\d{4}) 用 match.Groups["year"].Value 来读取。请先检查 match.Success,因为匹配失败时分组是空的,而不是 null。
C# 中如何用正则表达式替换文本?
Regex.Replace(input, pattern, replacement) 会替换每一个匹配。替换字符串可以引用分组:$1 表示编号分组,${name} 表示命名分组,$0 表示整个匹配。如果需要模板无法表达的逻辑,可以传入一个 lambda:Regex.Replace(text, @"\d+", m => (int.Parse(m.Value) * 2).ToString())。
C# 正则表达式如何忽略大小写?
传入 RegexOptions.IgnoreCase:Regex.IsMatch(input, "error", RegexOptions.IgnoreCase)。多个选项用 | 组合,例如 RegexOptions.IgnoreCase | RegexOptions.Multiline。你也可以在模式内部用 (?i) 打开它。
C# 中如何用正则表达式校验邮箱地址?
像 ^[^@\s]+@[^@\s]+\.[^@\s]+$ 这样的模式能发现明显的输入错误(缺少 @、含有空格、域名里没有点),对表单来说通常就足够了。正则表达式无法真正验证一个地址:完整的语法允许各种实用模式都处理不了的形式,而语法上合法的地址也可能并不存在。先检查基本格式,再发送一封验证邮件来确认。