Substring 返回一个由已有字符串的一部分组成的新字符串。它有两个重载:
s.Substring(startIndex):从startIndex到末尾。s.Substring(startIndex, length):从startIndex开始的length个字符。
索引从 0 开始,第二个参数是长度,而不是结束位置。这正是它与 JavaScript 的 substring(start, end) 和 Java 的 substring(begin, end) 的不同之处,也是移植代码时许多“差几位”错误的来源。
输出:
4271-BLK
SHOE
4271
BLK
0
SHOE-4271-BLK
和所有字符串方法一样,Substring 不会修改原字符串;它返回一个新字符串,所以要把结果赋值给变量。
ArgumentOutOfRangeException 以及如何避免
当请求的范围超出字符串时,Substring 会抛出 ArgumentOutOfRangeException:startIndex 为负数、startIndex 大于 Length、length 为负数,或者 startIndex + length 大于 Length。和 Python 的切片不同,它不会悄悄截断。
输出:
Substring(2, 4) -> "ffee"
Substring(2, 5) -> ArgumentOutOfRangeException
Substring(6) -> ""
Substring(7) -> ArgumentOutOfRangeException
Substring(-1) -> ArgumentOutOfRangeException
Substring(0, 10) -> ArgumentOutOfRangeException
常见的防护方法是用 Math.Min 把长度限制在字符串剩余的部分以内:
输出:
ffee
ea
[]
[]
对 null 字符串调用 Substring 抛出的是 NullReferenceException,而不是 ArgumentOutOfRangeException,所以这个辅助方法会先检查 null。
前 n 个和后 n 个字符
取前缀或后缀是 Substring 最常见的用法。当字符串比 n 短时,两者都会抛出异常,所以生产代码会先检查长度:
输出:
**** 1111
An Unexpecte...
Hi
Hi
如果你想去掉的是字符串的开头部分,单独用 Substring(n) 就够了:"#FF8800".Substring(1) 是 "FF8800"。相关的 Remove(start, count) 做的事情与 Substring 相反:它返回去掉了这个范围的字符串。
Substring 配合 IndexOf
实际中大多数子字符串都是找出来的,而不是数出来的。IndexOf 返回一个字符或字符串的位置(如果不存在则返回 -1),LastIndexOf 从末尾开始查找。把它们与 Substring 结合,就能围绕分隔符截取文本:
输出:
maya.lopez
example.com
pdf
report.final.v2
WARN
Disk almost full
计算遵循一条规则:要取位置 a 和 b 之间的文本(两端都不包含),起始位置是 a + 1,长度是 b - a - 1。
危险在于 IndexOf 返回 -1。email.Substring(0, -1) 会抛出异常,更糟的是,email.Substring(-1 + 1) 会悄悄返回整个字符串。一定要检查:
输出:
shop.io
(none)
用 Split 替代
当字符串是用分隔符隔开的字段列表时,Split 比一连串 IndexOf 和 Substring 调用更简单,也没有会算错的索引运算:
输出:
Ana Silva
Porto
maya.lopez
beach.jpg
Split 会分配一个数组,并为每个字段分配一个字符串,所以在热点循环中只截一刀时,IndexOf 加 Substring 开销更小。其他情况下,可读性更重要。更多选项(多个分隔符、去掉空项、最大数量)见 字符串方法。对于“ID: 后面的数字”这类模式,正则表达式通常是最清晰的工具。
范围运算符(C# 8 及更高版本)
C# 8 新增了索引和范围语法,适用于字符串和数组。^n 表示“倒数第 n 个”,a..b 是从 a 到 b(不包含 b)的范围,所以第二个数字是结束索引,而不是长度:
// C# 8 and later
string sku = "SHOE-4271-BLK";
string first4 = sku[..4]; // "SHOE" same as Substring(0, 4)
string middle = sku[5..9]; // "4271" same as Substring(5, 4)
string last3 = sku[^3..]; // "BLK" same as Substring(sku.Length - 3)
string noEnds = sku[1..^1]; // "HOE-4271-BL" drop first and last
char lastChar = sku[^1]; // 'K'
在字符串上,范围会被编译成一次 Substring 调用,所以它同样会分配新字符串,并在同样的越界情况下抛出 ArgumentOutOfRangeException。用哪种只是风格问题;当两端分别从不同方向计算时([1..^1]),范围写法更易读。
不复制的子字符串:Span
每次调用 Substring 都会分配一个新字符串。在对性能敏感的解析代码中(读取大文件、处理大量请求),.NET Core 2.1 及更高版本提供了 ReadOnlySpan<char>,它是原字符串某一部分的视图,不做任何复制:
ReadOnlySpan<char> line = "2026-09-24,42.50,EUR".AsSpan();
ReadOnlySpan<char> amount = line.Slice(11, 5); // "42.50", no allocation
decimal value = decimal.Parse(amount, provider: CultureInfo.InvariantCulture);
Span 不能存储在普通类的字段中,也不能跨 await 使用,所以它们适用于紧凑的同步代码。对于一般的应用代码,Substring 就是合适的工具。
常见错误
- 把结束索引当作长度传入。
s.Substring(2, 5)表示从索引 2 开始的五个字符,而不是“索引 2 到 5”。要取从索引a到索引b(不包含b)的文本,长度是b - a。 - 没有检查
IndexOf是否为 -1。 结果要么是异常,要么(加上+ 1后)意外得到整个字符串。 - 以为短字符串一定够长。 名字、编码或文件中的一行都可能比你预想的短。用
Math.Min限制,或者检查Length。 - 丢弃了结果。 单独一行写
s.Substring(1);没有任何用处。
常见问题
C# 中 Substring 是怎么工作的?
s.Substring(start) 返回从索引 start 到末尾的字符;s.Substring(start, length) 返回从 start 开始的 length 个字符。索引从 0 开始,第二个参数是长度,而不是结束索引:"Hello".Substring(1, 3) 是 "ell"。原字符串不会改变。
为什么 Substring 会抛出 ArgumentOutOfRangeException?
因为 start 是负数或大于字符串长度,或者 start + length 超出了末尾。最常见的原因是长度是按更长的字符串算出来的,或者把 IndexOf 返回的 -1 当作了起始位置。用 Math.Min(length, s.Length - start) 限制长度,并在使用 IndexOf 的结果之前先检查它。
C# 中如何获取字符串的后 n 个字符?
只要 n <= s.Length,s.Substring(s.Length - n) 就会返回最后 n 个字符。为了对短字符串也安全,请用 s.Length <= n ? s : s.Substring(s.Length - n)。在 C# 8 及更高版本中,也可以写 s[^n..],当 n 太大时它同样会抛出异常。
C# 中如何截取两个字符之间的子字符串?
用 IndexOf 找到两个位置,再取它们之间的部分:int start = s.IndexOf('(') + 1; int end = s.IndexOf(')', start); string inside = s.Substring(start, end - start);。在调用 Substring 之前,先检查每个 IndexOf 都找到了结果(不是 -1)。对于复杂的模式,正则表达式更清晰。
C# 中 Substring 的索引是从零开始的吗?
是的。第一个字符的索引是 0,最后一个是 Length - 1。Substring(0, 3) 返回前三个字符。把 Length 本身作为起始位置是允许的,会返回空字符串。