StringBuilder适用于循环内反复追加、拼接次数动态不可知或单次超百字符的场景;少于4次固定拼接应直接用+或$"";初始化宜预估容量,避免频繁realloc和线程不安全的Clear复用。

StringBuilder 什么时候该用,什么时候别硬上
字符串拼接少于 4 次、拼接内容固定或来自常量,直接用 + 或 $"" 就行——StringBuilder 反而多开销。它真正起作用的场景是:循环内反复追加、拼接次数动态不可知、单次拼接涉及上百字符以上。
常见错误现象:for 循环里写 result += item,跑着跑着变慢,GC 压力明显上升;这时候才是 StringBuilder 的发力点。
- 初始化时尽量预估容量:
new StringBuilder(1024)比无参构造更省 realloc - 避免在循环外反复
.Clear()复用实例——线程不安全,且容易漏重置Length - 不要把它当“万能字符串容器”:不支持直接索引赋值、不能当
string传参(得调.ToString())
Append 系列方法怎么选,参数类型真有影响
Append(string) 最常用,但如果你拼的是数字、布尔、枚举,别先 .ToString() 再传进去——Append(int)、Append(bool) 这些重载内部走的是无分配格式化路径,不触发临时字符串分配。
性能差异在高频调用时明显:10 万次拼接整数,用 Append(i) 比 Append(i.ToString()) 少约 2.3MB 托管堆分配。
-
AppendLine()自动加\r\n,比Append("\n")更跨平台,但注意 Windows 下是\r\n,Linux/macOS 是\n -
AppendFormat()能用,但格式字符串含变量多时,推荐改用Append($"{a}{b}")(C#10+ 插值字符串编译为Append链),更直观也略快 - 别用
Insert()或Remove()做频繁中间修改——底层是数组移动,复杂度 O(n),此时不如重建StringBuilder
String 和 StringBuilder 的本质区别不是“可变/不可变”,而是内存契约
string 不可变是表象,关键是它的生命周期由 GC 全权管理,每次拼接都产生新对象;StringBuilder 是个“缓冲区管理者”,内部维护 char[] 数组,Append 只往里填,不到容量才扩容。
这就带来两个容易被忽略的兼容性细节:
- 你拿到
StringBuilder实例,不能假设它的Length等于内部数组长度——Capacity可能远大于Length,序列化或调试时看到空余空间别慌 -
ToString()返回的是当前内容副本,后续对StringBuilder的修改不影响已生成的string;但反过来,别把ToString()结果长期缓存后还反复调它——那又回到字符串不可变的老路 - 跨线程共享
StringBuilder实例?不行。它不是线程安全的,没锁,也没原子操作保障
拼完记得 ToString(),但别在每一步都调
这是新手最常踩的坑:在循环里每拼一个字段就来一次 sb.ToString(),结果性能比纯 + 还差——因为每次调都复制整个缓冲区到新字符串。
正确姿势只在最终需要字符串值时调一次:var result = sb.ToString();
- 如果后续还要继续拼,别用
result去做条件判断再塞回sb——那是无效 round-trip - 如果只是日志输出调试,用
sb.ToString().Substring(0, Math.Min(200, sb.Length))截断即可,别为了“看一眼”触发全量拷贝 - 需要多次获取不同阶段的字符串?考虑用
Span+stackalloc做临时切片(高级用法,需unsafe上下文),但绝大多数场景没必要
真正麻烦的从来不是“会不会用”,而是“在哪儿截断 StringBuilder 生命周期”——它不像 string 那样天然适合函数边界和参数传递,一不留神就卡在中间状态里。











