类型 · 字符串

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 没生效?"——因为返回值被你丢了。字符串是恰好装着文本的值;把每个方法的返回值当成真正的产出。

为什么要不可变?

代价:在循环里拼字符串

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)+ / 插值可读;反正只有一次分配
循环里攒文本StringBuilderO(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);        // 那现在呢?