C# StringBuilder 详解:字符串拼接为什么总说要用它?

C# StringBuilder 详解:常用 API、缓冲区扩容原理与 string 对比(入门+速查)

导语:string 就像一块石碑,字一旦刻上去就改不了,想改只能重新刻一块;而 StringBuilder 是一块黑板,提前预留了板面,你可以在上面反复写、反复擦,写完再一次性誊抄成正式文本。本文就带你彻底搞懂:循环里拼字符串为什么慢、StringBuilder 怎么用、它底层是怎么做到的,以及哪些场景其实根本不需要它。

一、什么是 StringBuilder

StringBuilder 是 C# 专门用来高效地拼接、修改字符串 的一个类,位于 System.Text 命名空间下。

它的核心特点只有一个:可变。

我们平时用的 string 是"不可变"的(immutable),任何修改都会生成一个新字符串;而 StringBuilder 内部维护着一块可以反复写入的字符缓冲区,在你调用 ToString() 之前,不会产生新的字符串对象。

一句话总结二者的分工:

  • string:用来存放、查找、比较、传递文本。
  • StringBuilder:用来拼装、编辑 文本,拼好之后再转成 string。

二、为什么需要 StringBuilder:从 string 的"脾气"说起

2.1 string 是不可变的

什么叫不可变?看下面这段代码:

csharp 复制代码
string s = "ABC";
string s2 = s;      // s2 和 s 指向同一个字符串 "ABC"
s += "DEF";         // 注意:这里不是在原字符串后面写字,而是造了一个新字符串

Console.WriteLine(s);   // 输出:ABCDEF
Console.WriteLine(s2);  // 输出:ABC(s2 完全没受影响)

s += "DEF" 看起来是"在 s 后面加了几个字",但底层实际做的是:

  1. 申请一块新的内存;
  2. 把旧字符串 "ABC" 和新内容 "DEF" 一起拷贝过去;
  3. 让变量 s 指向新字符串 "ABCDEF";
  4. 原来的 "ABC" 没人用了,等着垃圾回收器(GC)回收。

也就是说,每拼接一次,就会产生一个被丢弃的旧对象。

【插图 1:string 拼接产生临时对象示意图】

2.2 循环里拼接,性能问题被放大

一次两次拼接无所谓,编译器甚至会帮你优化。但如果放在循环里,比如拼接 5 万次:

csharp 复制代码
using System;
using System.Diagnostics;   // 计时用的 Stopwatch 在这个命名空间
using System.Text;          // StringBuilder 在这个命名空间

class Program
{
    static void Main(string[] args)
    {
        const int count = 50000;   // 拼接次数

        // 方式一:用 string 直接 +=
        Stopwatch sw1 = Stopwatch.StartNew();
        string s = "";
        for (int i = 0; i < count; i++)
        {
            s += i;   // 每次循环都在造新字符串、拷贝旧内容
        }
        sw1.Stop();

        // 方式二:用 StringBuilder
        Stopwatch sw2 = Stopwatch.StartNew();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < count; i++)
        {
            sb.Append(i);   // 内容写进缓冲区,不产生新字符串
        }
        string result = sb.ToString();  // 最后只生成一次 string
        sw2.Stop();

        Console.WriteLine($"string 拼接耗时:{sw1.ElapsedMilliseconds} ms");
        Console.WriteLine($"StringBuilder 拼接耗时:{sw2.ElapsedMilliseconds} ms");
        Console.ReadKey();
    }
}

【插图 2:两种拼接方式的控制台运行结果】

运行后你会直观地看到:循环次数越大,两者的差距越夸张,5 万次循环时往往能相差几百上千倍。原因很简单:

  • string += 拼到第 N 次时,要把前 N-1 个字符全部拷贝一遍,越往后拷贝量越大,总工作量大约是 1+2+3+...+N,而且会产生 N 个等待回收的对象。
  • StringBuilder 只往缓冲区里写,整个过程最终只生成一个字符串。

补充:如果只是 "a" + "b" + "c" 这种固定的、次数很少 的拼接,编译器会自动优化成一次 string.Concat,不会真的产生一堆中间对象,这种场景就不必动用 StringBuilder,后面"常见坑"一节还会细说。

三、准备:创建 StringBuilder

使用前先引入命名空间:

csharp 复制代码
using System.Text;

常用的构造方式有四种:

csharp 复制代码
// 1. 创建一个空的 StringBuilder,之后再往里追加内容
StringBuilder sb1 = new StringBuilder();

// 2. 创建时带上初始字符串
StringBuilder sb2 = new StringBuilder("你好");

// 3. 指定初始容量(知道大概要拼多长时,提前预留位置,减少后续扩容次数)
StringBuilder sb3 = new StringBuilder(1024);

// 4. 同时指定初始字符串和容量
StringBuilder sb4 = new StringBuilder("你好", 1024);

构造函数归纳:

构造写法 作用
new StringBuilder() 创建空对象,默认初始容量为 16
new StringBuilder("初始内容") 带初始字符串
new StringBuilder(1024) 指定初始容量
new StringBuilder("初始内容", 1024) 初始字符串 + 容量
new StringBuilder(1024, 1024 * 1024) 同时指定初始容量和最大容量(了解即可)

四、核心用法:常用 API 全家桶

4.1 追加内容:Append、AppendLine、AppendFormat、AppendJoin

csharp 复制代码
StringBuilder sb = new StringBuilder();

sb.Append("姓名:");        // 追加内容,不换行
sb.Append("张三");
sb.AppendLine();            // 只追加一个换行符(相当于回车换行)
sb.AppendLine("年龄:18");  // 追加一行,末尾自动带换行

// 按占位符格式追加,用法和 string.Format 完全一样
sb.AppendFormat("成绩:{0:F1} 分", 95.5);
sb.AppendLine();

// 用分隔符拼接一组数据,类似 string.Join,免去手动加分隔符
string[] hobbies = { "看书", "跑步", "编程" };
sb.AppendJoin("、", hobbies);

string result = sb.ToString();
Console.WriteLine(result);

输出:

text 复制代码
姓名:张三
年龄:18
成绩:95.5 分
看书、跑步、编程

小提示:Append 有几十个重载,字符串、数字、bool、char、DateTime 等基本类型都能直接追加,不需要自己先 ToString(),它内部会帮你转。

4.2 插入内容:Insert

Insert 可以在指定下标位置插入内容,下标从 0 开始。

csharp 复制代码
StringBuilder sb = new StringBuilder("Hello World");
sb.Insert(5, ",");    // 在下标 5 的位置插入一个逗号
Console.WriteLine(sb.ToString());  // 输出:Hello, World

4.3 删除与替换:Remove、Replace、Clear

csharp 复制代码
StringBuilder sb = new StringBuilder("Hello, World!");

// Remove(起始下标, 删除长度):从下标 5 开始,删除 2 个字符(删掉逗号和空格)
sb.Remove(5, 2);
Console.WriteLine(sb.ToString());  // 输出:HelloWorld!

// Replace:把所有匹配的字符(或字符串)全部替换掉
sb.Replace('o', '0');              // 字符重载:所有 'o' 换成 '0'
Console.WriteLine(sb.ToString());  // 输出:Hell0W0rld!

sb.Replace("World", "C#");         // 字符串重载:所有 "World" 换成 "C#"

sb.Clear();                        // 清空全部内容,Length 变为 0

4.4 读取与转换:Length、索引器、ToString

csharp 复制代码
StringBuilder sb = new StringBuilder("ABCDEF");

Console.WriteLine(sb.Length);   // 输出:6(当前字符个数)
Console.WriteLine(sb[0]);       // 输出:A(可以像数组一样按下标读字符)

sb[0] = 'a';                    // 还可以直接修改某个位置的字符------这就是"可变"
Console.WriteLine(sb.ToString()); // 输出:aBCDEF

// 把 Length 设小,还能实现"截断"效果
sb.Length = 3;
Console.WriteLine(sb.ToString()); // 输出:aBC

重点:StringBuilder 不是字符串,编辑完成后必须调用 ToString() 转成正式的 string,才能用于赋值、传参、返回等场合。

csharp 复制代码
StringBuilder sb = new StringBuilder("正在拼接...");
sb.Append("完成");

string text = sb.ToString();   // 正确:转成 string
// textBox1.Text = sb;        // 错误:编译都通不过,Text 属性只接受 string
textBox1.Text = sb.ToString(); // 正确

4.5 容量控制:Capacity、EnsureCapacity

StringBuilder 内部有一块缓冲区,就像黑板有大小。两个相关概念:

  • Length:当前实际写了多少个字符(用了多少板面)。
  • Capacity:缓冲区最多能放多少个字符(黑板总大小)。

当 Length 要超过 Capacity 时,它会自动扩容。如果你事先知道内容大概有多长,可以提前预留容量,避免扩容时的数据拷贝:

csharp 复制代码
StringBuilder sb = new StringBuilder();
sb.EnsureCapacity(1000);   // 保证容量至少为 1000,循环前调用一次即可

// 也可以在构造时直接指定容量,效果一样
StringBuilder sb2 = new StringBuilder(1000);

4.6 链式调用

Append、AppendLine、Insert、Remove、Replace 这些方法的返回值都是 StringBuilder 自己(也就是 this),所以可以像流水一样一直点下去:

csharp 复制代码
string text = new StringBuilder()
    .AppendLine("第一行")
    .AppendLine("第二行")
    .Append("第三行")
    .ToString();

4.7 常用 API 速查表

【插图 3:StringBuilder 常用 API 体系图】

方法 / 属性 作用 备注
Append(value) 追加内容 重载非常多,基本类型都能直接追加
AppendLine() 追加一个换行符 无参时只换行
AppendLine(value) 追加内容并换行 写一行文本常用
AppendFormat(fmt, args) 按占位符格式追加 用法同 string.Format
AppendJoin(sep, values) 用分隔符拼接多个值 类似 string.Join
Insert(index, value) 在指定下标处插入 下标从 0 开始
Remove(start, length) 从指定位置删除若干字符 按"位置+长度",不是按内容
Replace(old, new) 全部替换 有 char 和 string 两个重载
Clear() 清空内容 清空后 Length = 0,容量保留
ToString() 转成 string 拼完务必调用
Length 当前字符个数 可读可写,设小等于截断
Capacity 缓冲区容量 可读可写
[index] 索引器,读写单个字符 只能访问已存在的位置
EnsureCapacity(n) 保证容量至少为 n 提前预留,减少扩容

五、原理:StringBuilder 为什么快

5.1 缓冲区与扩容

StringBuilder 内部维护着一块字符缓冲区(你可以理解为一个 char 数组):

  • 追加内容时,直接把字符写进缓冲区的空位里,不产生新字符串。
  • 当缓冲区放不下时,它才申请一块更大的新缓冲区,把旧内容搬过去,然后继续写,这个过程叫"扩容"。
  • 最后调用 ToString(),才一次性生成正式的 string。

【插图 4:StringBuilder 缓冲区与扩容原理示意图】

扩容不是每次追加都发生,而是"容量不够时才扩一次",所以即使循环拼接 5 万次,真正的扩容也只有十几次,产生的临时对象数量可以忽略不计。

补充(了解即可,不影响使用):在较老的 .NET Framework 中,扩容是"申请一块翻倍大小的单块缓冲区并拷贝全部内容";在 .NET Core 2.1 / .NET 5 及更高版本中,内部改成了分块链式结构,容量不够时直接挂上一块新的字符块,连"拷贝全部内容"都省掉了。对使用者来说,记住"往缓冲区写、不够才扩容、最后 ToString"这个模型就足够了。

5.2 与 string 的本质对比

对比项 string StringBuilder
可变性 不可变,改一次造一个新对象 可变,直接在缓冲区上改
命名空间 System System.Text
每次拼接 申请新内存、拷贝全部内容 写入缓冲区,不产生新字符串
擅长的操作 查找、截取、比较、分割、正则匹配 追加、插入、删除、替换等编辑操作
适用场景 少量、固定次数的拼接;文本处理 循环拼接、大量动态文本拼装
线程安全 不可变,天然安全 不是线程安全的,多线程共用需加锁

5.3 什么场景该用谁

推荐用 StringBuilder 的场景:

  • 循环里拼接字符串(最典型的场景);
  • 逐行读取文本文件、把多行拼成一个大字符串;
  • 动态拼接 SQL、HTML、JSON、报表、日志等。

直接用 string 更合适的场景:

  • 固定几行文本、一次性拼接,用 + 或字符串插值 $"" 更直观;
  • 拼完之后还要做 Split、Substring、Trim、IndexOf、正则匹配等操作------这些 StringBuilder 基本不提供,要转成 string 再处理。

六、常见坑

  1. 用完忘记 ToString()。 StringBuilder 不是 string,直接给 TextBox.Text、方法参数赋值会编译报错。养成"拼完立刻 ToString()"的习惯。

  2. 两种极端用法都不对:少量拼接乱用、循环拼接没用对。 "a" + "b" + "c" 这种固定拼接编译器会优化成一次 string.Concat,写日志、拼路径这种一锤子买卖,用 $"姓名:{name},年龄:{age}" 更直观,没必要动用 StringBuilder;而真正循环拼接时,要在循环外创建一次、循环里只 Append、循环结束后统一 ToString(),把 new 和 ToString() 都写进循环体,等于把它的优势完全浪费了。

  3. 以为 Append 会自动换行。 Append 只追加内容,换行必须用 AppendLine(),或自己追加 Environment.NewLine、"\r\n"。

  4. Remove 按"位置+长度"删,Replace 是全部替换。 二者都不是按内容操作:想删除某个子串,要先用 ToString().IndexOf(...) 找到位置再算长度;Replace 没有"只替换第一个"的重载,只替换一次时需要自己定位后用 Remove + Insert。

  5. 多线程共用同一个实例会出问题。 StringBuilder 不是线程安全的,多个线程同时 Append 可能导致内容错乱甚至异常。做法是加锁(lock),或者让每个线程各用一个实例,最后再合并。

七、小结

  1. string 是不可变的,每次修改都生成新对象;循环拼接时反复拷贝旧内容,会产生大量临时对象,次数越多越慢。
  2. StringBuilder 在 System.Text 下,内部维护一块可反复写入的字符缓冲区 ,追加不产生新字符串,只有容量不够时才扩容,最后 ToString() 一次成型。
  3. 常用 API:追加用 Append / AppendLine / AppendFormat / AppendJoin,插入用 Insert,删除替换用 Remove / Replace / Clear,别忘了最终 ToString();已知内容长度时用构造函数或 EnsureCapacity 提前预留容量。
  4. 选型原则:循环、大量、动态拼接用 StringBuilder;少量、固定、需要文本处理用 string。
  5. 牢记高频坑:忘记 ToString()、Append 不换行、Clear() 不释放容量、多线程共用不加锁。
相关推荐
weixin_4481199440 分钟前
Datawhale DeepSeek Harness 技术入门与架构原理 笔记3
笔记·架构
Wang's Blog44 分钟前
Java 项目实战: 交友项目 - 今日佳人:需求分析
java·开发语言·交友
老孟说禅1 小时前
自动化测试入门(一)
开发语言·python
果壳science1 小时前
关于光子、量子猜想
笔记·科技·学习·创业创新·学习方法
淡海水1 小时前
06-03-源码-时间分片与调度策略
unity·c#·gc
余额瞒着我当琳1 小时前
C++11,发展历史浅谈,初始化列表和对象初始化,右值引用和移动语义
java·开发语言·c++·算法
愤怒火龙果1 小时前
蓝队笔记整理01 C2 远控与 Webshell 流量特征
笔记·网络安全
学逆向的1 小时前
C 宏函数
c语言·开发语言·网络安全