类型 · 字符串
C# 里 string 与 StringBuilder
解释一切的那句话:C# 字符串不可变——没有任何字符串方法会修改字符串,它们全都返回一个新的。StringBuilder 的存在就是为了这个事实。
不可变性,演示出来
运行这段,看一个方法调用什么都没干:
string s = "hello";
s.Replace("l", "L"); // 返回一个新字符串——s 纹丝不动
Console.WriteLine(s);
string s = "hello";
s = s.Replace("l", "L"); // 把新字符串接回来
Console.WriteLine(s); // heLLo
s = s.ToUpper(); // 每个字符串方法都是同一条规则
Console.WriteLine(s); // HELLO
// 原始字面量从未改变——被制造出来的是新字符串:
Console.WriteLine("original still intact: " + ("hello" == "hello"));
想通之后,一整个 bug 家族就消失了:"为什么我的 Replace 没生效?"——因为返回值被你丢了。字符串是恰好装着文本的值;把每个方法的返回值当成真正的产出。
为什么要不可变?
- 共享安全。把字符串传给任何方法,对方都改不了"你那份"——因为根本没有可改的东西。(类正相反:引用别名。)
- 免费驻留。相同的字面量可以安全共享同一份存储——这就是为什么
"apple" == "apple"哪怕按引用比较也能是 True。 - 可哈希。不可变的键哈希值不会漂移——这正是字符串能当
Dictionary键的原因。
代价:在循环里拼字符串
string msg = "";
for (int i = 1; i <= 5; i++)
{
msg += i + ","; // 每次 += 都造一个全新字符串:旧的 + 新片段
}
Console.WriteLine(msg);
五次循环,无所谓。算笔账:第 n 轮要拷贝前 n−1 轮攒下的全部字符才能造出新串。总拷贝量按 1+2+3+…+n ≈ n²/2 个字符增长。n 到十万就是几十亿次字符拷贝,外加一座垃圾山。StringBuilder 就是为修这个而生的。
StringBuilder:可变的缓冲区
StringBuilder 维护内部缓冲区,直接往里追加——每次 append 不造新串、不拷贝旧内容。只在最后 ToString() 那一刻付一次账:
// StringBuilder 不在浏览器引擎的教学子集里——在 Visual Studio 里原样可跑
using System.Text;
var sb = new StringBuilder();
for (int i = 1; i <= 100000; i++)
{
sb.Append(i);
sb.Append(',');
}
string msg = sb.ToString(); // 只在最后造一次字符串
在引擎里把循环版跑到 n = 100000 感受一下差距;然后相信 StringBuilder 眨眼间就能干完同样的活。(引擎里的等价物是 List<T> + string.Join:var parts = new List<string>(); …… string.Join(",", parts)。)
那么到底用哪个?
| 场景 | 用 | 为什么 |
|---|---|---|
两三个片段、拼一次(name + " is " + age) | + / 插值 | 可读;反正只有一次分配 |
| 循环里攒文本 | StringBuilder | O(n) 而不是 O(n²) |
| 拼接集合 | string.Join | 专用工具,一次调用完事 |
| 不可变的文本值 | string | 它就是默认——而且是安全网 |
经验法则:片段少 → 插值;循环 → StringBuilder;集合 → Join。别给两三个片段的情况提前优化——$"{name} is {age}" 写得最快,跑得也够快。
一分钟自测
两行都有输出——去练习场运行前先各预测一次:
string s = "cat";
s.Replace("c", "b");
Console.WriteLine(s); // "bat" 还是 "cat"?(不可变性)
s = s.Replace("c", "b");
Console.WriteLine(s); // 那现在呢?