详情

首页手游攻略 C#中StringBuilder的核心机制与最佳实践实用指南

C#中StringBuilder的核心机制与最佳实践实用指南

佚名 2026-10-28 10:50:02

平时做技术实践时,很多问题不是概念不会,而是细节没串起来。拿“C#中StringBuilder的核心机制与最佳实践”来说,它看着像小点,放到项目里常会牵出环境、配置、兼容性和维护成本。下面按实际采用顺序,把思路、关键写法和容易踩坑的地方讲清楚,便于大家直接对照操作。

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

一、什么是 StringBuilder

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

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

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

一句话总结二者的分工:

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

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

2.1 string 是不可变的

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

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 万次:

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

采用前先引入命名空间:

using System.Text;

常用的构造方式有四种:

// 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

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);

输出:

姓名:张三
年龄:18
成绩:95.5 分
看书、跑步、编程

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

4.2 插入内容:Insert

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

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

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

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

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,才能用来赋值、传参、得到等场合。

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 时,它会自动扩容。如果你事先知道内容大概有多长,能够提前预留容量,避免扩容时的数据拷贝:

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

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

4.6 链式调用

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

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 的本质对比

对比项stringStringBuilder
可变性不可变,改一次造一个新对象可变,直接在缓冲区上改
命名空间SystemSystem.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、"rn"。
  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() 不释放容量、多线程共用不加锁。

理解这一步时,总的来说,C# StringBuilder核心机制与实践适合结合实际项目边做边理解。先抓住核心思路,再逐步补上细节和边界处理,最后效果会更稳定,也更容易复用。

点击查看更多
推荐专题
热门阅读