C# StringBuilder源码分析

📅 发布时间:2026/7/8 23:57:24 👁️ 浏览次数:
C# StringBuilder源码分析
在 .NET 中StringBuilder是一个用于高效构建字符串的重要类。它通过避免频繁创建新字符串对象从而优化了性能。但其背后的实现机制却并不简单。一、核心字段与属性解析StringBuilder内部使用了字符数组char[]来存储字符串数据并通过链表的方式管理多个“块”Chunk以提升拼接效率。主要字段internalchar[]m_ChunkChars;// 当前块的字符数组internalintm_ChunkLength;// 当前块中已使用的字符数internalintm_ChunkOffset;// 当前块在整个字符串中的起始位置internalintm_MaxCapacity;// 最大容量默认为 int.MaxValueinternalconstintDefaultCapacity16;// 默认初始容量internalconstintMaxChunkSize8000;// 单个 Chunk 的最大长度Length 属性publicintLength{getm_ChunkOffsetm_ChunkLength;}表示当前整个字符串的总长度。二、构造函数分析1. 默认构造函数publicStringBuilder(){m_MaxCapacityint.MaxValue;m_ChunkCharsnewchar[DefaultCapacity];// 初始大小为16}默认分配一个长度为 16 的字符数组。2. 带字符串参数的构造函数publicStringBuilder(stringvalue,intstartIndex,intlength,intcapacity){...m_ChunkCharsGC.AllocateUninitializedArraychar(capacity);...}根据传入字符串的长度和指定容量选择较大的值作为初始容量避免多次扩容。3. 复制构造函数用于链表节点创建privateStringBuilder(StringBuilderfrom){m_ChunkLengthfrom.m_ChunkLength;m_ChunkOffsetfrom.m_ChunkOffset;m_ChunkCharsfrom.m_ChunkChars;m_ChunkPreviousfrom.m_ChunkPrevious;...}这个构造函数用于创建新的 Chunk 节点是链表结构的关键。三、Append 方法的工作原理以Append(char value, int repeatCount)为例来看StringBuilder如何处理追加操作publicStringBuilderAppend(charvalue,intrepeatCount){//省略边界检查代码intindexm_ChunkLength;while(repeatCount0){if(indexm_ChunkChars.Length){m_ChunkChars[index]value;--repeatCount;}else{m_ChunkLengthindex;ExpandByABlock(repeatCount);// 扩容并创建新 ChunkDebug.Assert(m_ChunkLength0);index0;}}m_ChunkLengthindex;returnthis;}核心逻辑如果当前字符数组还有空间则直接插入字符。如果空间不足调用ExpandByABlock()创建新 Chunk并将其链接到当前 Chunk 的前面。四、ExpandByABlock 方法详解该方法负责创建新的 Chunk 并更新当前 Chunk 的状态privatevoidExpandByABlock(intminBlockCharCount){intnewBlockLengthMath.Max(minBlockCharCount,Math.Min(Length,MaxChunkSize));char[]chunkCharsGC.AllocateUninitializedArraychar(newBlockLength);m_ChunkPreviousnewStringBuilder(this);// 创建前驱节点m_ChunkOffsetm_ChunkLength;m_ChunkLength0;m_ChunkCharschunkChars;}关键步骤计算新 Chunk 的大小不超过MaxChunkSize默认 8000也不小于所需字符数。分配新内存。创建前驱节点将当前 Chunk 封装成一个新的StringBuilder实例并赋值给m_ChunkPrevious。更新偏移量和长度当前 Chunk 清空准备写入新数据。切换字符数组将新分配的数组设为当前 Chunk 使用。五、图解1、初始状态默认构造函数创建 StringBuildervarsbnewStringBuilder();内部结构sb(current)│ ├── m_ChunkChars:[_ _ _ _ _ _ _ _ _ _ _ _ _ ___]← 默认大小为16├── m_ChunkLength:0├── m_ChunkOffset:0└── m_ChunkPrevious:null此时没有数据只是一个空的字符数组。2、第一次 Append 操作添加 “HELLO”sb.Append(HELLO);内部结构sb(current)│ ├── m_ChunkChars:[H E L L O _ _ _ _ _ _ _ _ ___]├── m_ChunkLength:5├── m_ChunkOffset:0└── m_ChunkPrevious:null此时字符数组还有 11 个空间可用。3、继续 Append添加 “WORLD”sb.Append(WORLD);字符串长度为 5刚好可以放入剩余空间中。内部结构sb(current)│ ├── m_ChunkChars:[H E L L O W O R L D _ _ _ _ ___]├── m_ChunkLength:10├── m_ChunkOffset:0└── m_ChunkPrevious:null此时字符数组还剩 6 个空位。4、扩容触发再次 Append 超出当前容量sb.Append(THIS_IS_A_LONG_STRING);// 长度超过剩余空间此时字符数组只剩 6 个位置但要插入的字符串长度为 20因此需要扩容。步骤解析更新当前 Chunk 的状态将m_ChunkLength设置为当前已使用的长度10。记录当前 Chunk 的偏移量为m_ChunkOffset m_ChunkLength 0 10 10。清空当前 Chunk 的使用长度设为 0。创建新 Chunk 并设置为前驱节点创建一个新的StringBuilder实例其字符数组大小根据所需内容和最大块大小8000决定。将当前 Chunk 的引用赋值给新 Chunk 的m_ChunkPrevious。切换当前 Chunk 到新分配的数组。扩容后结构newChunk(current)│ ├── m_ChunkChars:[T H I S _ I S _ A _ L O N G _ S T R I N G...]假设新块大小为32 ├── m_ChunkLength:20├── m_ChunkOffset:10└── m_ChunkPrevious:↓ oldChunk │ ├── m_ChunkChars:[H E L L O W O R L D _ _ _ _ ___]├── m_ChunkLength:10├── m_ChunkOffset:0└── m_ChunkPrevious:null注意此时sb变量指向的是newChunk也就是最后一个 Chunk。5、继续 Append 更多内容每次扩容都会在链表头部插入新的 Chunk并保留对旧 Chunk 的引用。最终形成一个“逆向链表”结构sb(current)→ newChunk3 → newChunk2 → oldChunk →null每个 Chunk 包含一部分字符串内容且可以通过m_ChunkPrevious向前追溯完整字符串。六、为什么使用逆向链表每个StringBuilder对象维护一个指向“前一个节点”的引用 (m_ChunkPrevious)而不是常见的“后一个节点”。这样做的好处尾部追加操作更高效由于用户总是从最后一个 Chunk 添加数据采用“逆向链表”可以快速定位到最后一个节点无需遍历整个链表。时间复杂度为 O(1)每次添加新 Chunk 都是在当前节点的基础上创建前驱节点无需查找最后一个节点。相比之下如果使用正向链表每个节点保存下一个节点引用则每次添加都需要遍历到末尾时间复杂度为 O(n)性能下降明显。七、链表结构带来的代价虽然链表提升了追加效率但也带来了一些缺点无法随机访问不能像数组一样直接通过索引访问某个字符。读取效率较低若需要从中间或开头插入数据需遍历整个链表效率不如单一数组。因此StringBuilder更适合尾部拼接的场景而不适合频繁的随机修改。八、最常用的Append方法StringBuilder.Append(string value)是最常用的方法Append(string? value)方法/// summary/// 将一个字符串追加到当前 StringBuilder 的末尾。/// /summary/// param namevalue要追加的字符串可以为 null。/param/// returns当前的 StringBuilder 实例以支持链式调用。/returnspublicStringBuilderAppend(string?value){// 如果传入的字符串为 null则直接返回不进行任何操作if(value!null){// 获取当前 chunk 的字符数组和已使用的长度char[]chunkCharsm_ChunkChars;intchunkLengthm_ChunkLength;intvalueLenvalue.Length;// 判断当前 chunk 是否还有足够的空间容纳要追加的字符串// 使用 uint 类型进行加法溢出检查确保不会超出数组长度if(((uint)chunkLength(uint)valueLen)(uint)chunkChars.Length){// 如果要追加的字符串长度较小最多两个字符则直接逐个字符复制if(valueLen2){if(valueLen0){// 追加第一个字符chunkChars[chunkLength]value[0];}if(valueLen1){// 追加第二个字符chunkChars[chunkLength1]value[1];}}else{// 如果要追加的字符串较长使用高效的固定指针方式复制字符unsafe{// 固定字符串和当前 chunk 的内存地址fixed(char*valuePtrvalue)fixed(char*destPtrchunkChars[chunkLength]){// 使用内部高效复制方法将字符复制到当前 chunk 中string.wstrcpy(destPtr,valuePtr,valueLen);}}}// 更新当前 chunk 已使用的字符数m_ChunkLengthchunkLengthvalueLen;}else{// 当前 chunk 空间不足调用 AppendHelper 方法进行扩容和追加AppendHelper(value);}}returnthis;}AppendHelper(string value)方法/// summary/// 用于处理当前 chunk 空间不足时的追加操作。/// 会分配新的 chunk 并将字符串内容复制进去。/// /summary/// param namevalue要追加的字符串非 null。/paramprivatevoidAppendHelper(stringvalue){unsafe{// 固定字符串的内存地址以便进行指针操作fixed(char*valueCharsvalue){// 调用 Append(char*, int) 方法进行实际的追加操作Append(valueChars,value.Length);}}}Append(char* value, int valueCount)方法/// summary/// 将一个字符指针指向的缓冲区内容追加到当前 StringBuilder 的末尾。/// /summary/// param namevalue指向要追加的字符缓冲区的指针。/param/// param namevalueCount要追加的字符数量。/param/// returns当前的 StringBuilder 实例以支持链式调用。/returns/// exception crefArgumentOutOfRangeException当 valueCount 为负数或超过最大容量时抛出。/exception[CLSCompliant(false)]publicunsafeStringBuilderAppend(char*value,intvalueCount){// 不检查 value 是否为 null因为访问 null 指针会自动抛出 NullReferenceException// 检查 valueCount 是否为负数防止非法参数if(valueCount0){thrownewArgumentOutOfRangeException(nameof(valueCount),SR.ArgumentOutOfRange_NegativeCount);}// 计算新字符串的总长度当前长度 要追加的字符数intnewLengthLengthvalueCount;// 检查是否超过最大容量m_MaxCapacity或发生整数溢出if(newLengthm_MaxCapacity||newLengthvalueCount){thrownewArgumentOutOfRangeException(nameof(valueCount),SR.ArgumentOutOfRange_LengthGreaterThanCapacity);}// 尝试将数据直接追加到当前 chunk 的剩余空间中intnewIndexvalueCountm_ChunkLength;// 如果当前 chunk 有足够的空间容纳所有数据直接复制if(newIndexm_ChunkChars.Length){ThreadSafeCopy(value,m_ChunkChars,m_ChunkLength,valueCount);m_ChunkLengthnewIndex;// 更新当前 chunk 使用的字符数}else{// 当前 chunk 空间不足只能先复制一部分// 计算当前 chunk 中剩余可用空间intfirstLengthm_ChunkChars.Length-m_ChunkLength;if(firstLength0){// 复制第一部分到当前 chunkThreadSafeCopy(value,m_ChunkChars,m_ChunkLength,firstLength);m_ChunkLengthm_ChunkChars.Length;// 当前 chunk 已满}// 剩余要复制的字符数intrestLengthvalueCount-firstLength;// 扩展 StringBuilder添加一个新的 chunkExpandByABlock(restLength);Debug.Assert(m_ChunkLength0,添加新 chunk 后chunk 的长度应为 0表示空块);// 将剩余部分复制到新的 chunk 中ThreadSafeCopy(valuefirstLength,m_ChunkChars,0,restLength);m_ChunkLengthrestLength;// 更新新 chunk 的使用长度}// 验证内部状态是否一致调试用AssertInvariants();returnthis;}ExpandByABlock(int minBlockCharCount)方法/// summary/// 将当前 chunk 的字符转移到一个新的 chunk 中并为当前 chunk 分配一个新的字符缓冲区。/// 这是扩容机制的一部分用于支持 StringBuilder 的动态增长。/// /summary/// param nameminBlockCharCount新 chunk 缓冲区的最小容量。/param/// remarks/// - 此方法要求当前 chunk 已满即 m_ChunkLength m_ChunkChars.Length。/// - 假设当前 chunk 是链表中的最后一个 chunk。/// /remarksprivatevoidExpandByABlock(intminBlockCharCount){// 调试断言确保当前 chunk 已满容量 长度Debug.Assert(CapacityLength,nameof(ExpandByABlock) should only be called when there is no space left.);// 调试断言确保请求的最小容量是正数Debug.Assert(minBlockCharCount0);// 验证当前 chunk 的状态是否合法调试用AssertInvariants();// 检查扩容后是否会超过最大容量或发生整数溢出if((minBlockCharCountLength)m_MaxCapacity||minBlockCharCountLengthminBlockCharCount){thrownewArgumentOutOfRangeException(requiredLength,SR.ArgumentOutOfRange_SmallCapacity);}// 计算新 chunk 的容量// - 至少为 minBlockCharCount满足当前需求// - 如果当前字符串长度较小则取当前长度实现“容量翻倍”策略// - 但不能超过 MaxChunkSize防止分配过大内存块intnewBlockLengthMath.Max(minBlockCharCount,Math.Min(Length,MaxChunkSize));// 检查是否会溢出 int.MaxValue逻辑总长度if(m_ChunkOffsetm_ChunkLengthnewBlockLengthnewBlockLength){thrownewOutOfMemoryException();}// 提前分配新的字符数组避免在分配失败时留下不一致的状态char[]chunkCharsGC.AllocateUninitializedArraychar(newBlockLength);// 创建一个新的 StringBuilder 实例新 chunk并将当前 chunk 设置为其前一个 chunkm_ChunkPreviousnewStringBuilder(this);// 更新当前 chunk 的偏移量当前 chunk 的字符总数 前面所有 chunk 的字符总数m_ChunkOffsetm_ChunkLength;// 当前 chunk 已经“转移”了所有字符因此长度清零m_ChunkLength0;// 将当前 chunk 的字符数组指向新分配的缓冲区m_ChunkCharschunkChars;// 再次验证当前 chunk 的状态是否合法调试用AssertInvariants();}ExpandByABlock方法是StringBuilder扩容机制的核心部分之一原理和Append(char value, int repeatCount)类似它会创建一个新的 chunk并将当前 chunk 的数据“转移”过去。然后将当前 chunk清空指向一个新分配的缓冲区准备接收新数据。通过这种方式StringBuilder可以高效地链式增长而不会频繁复制整个字符串。九、ToString方法详解publicoverridestringToString(){// 断言检查确保当前 StringBuilder 的内部状态是合法的调试时使用AssertInvariants();// 如果总长度为 0直接返回空字符串避免不必要的操作if(Length0){returnstring.Empty;}// 快速分配一个指定长度的字符串用于最终结果// string.FastAllocateString 是内部方法用于高效分配字符串空间stringresultstring.FastAllocateString(Length);// 使用 unsafe 代码块允许使用指针进行高效内存操作unsafe{// 将结果字符串的指针固定防止在 GC 中被移动否则指针会失效fixed(char*destinationPtrresult){// 从当前 chunk 开始沿着链表向前遍历所有 chunksStringBuilder?chunkthis;// 循环处理每一个 chunkdo{// 只有当前 chunk 中有数据才进行复制if(chunk.m_ChunkLength0){// 将 chunk 中的字段复制到局部变量中// 这样可以避免在多线程环境下字段被修改导致的数据不一致问题char[]sourceArraychunk.m_ChunkChars;intchunkOffsetchunk.m_ChunkOffset;// 当前 chunk 在整个字符串中的起始位置intchunkLengthchunk.m_ChunkLength;// 当前 chunk 中实际存储的字符数// 边界检查确保不会越界访问源数组和目标字符串if((uint)(chunkLengthchunkOffset)(uint)result.Length(uint)chunkLength(uint)sourceArray.Length){// 将源字符数组的指针固定fixed(char*sourcePtrsourceArray[0]){// 将当前 chunk 的字符复制到最终字符串的对应位置// destinationPtr chunkOffset 表示目标字符串中的偏移位置// sourcePtr 是源字符数组的起始地址// chunkLength 是要复制的字符数量string.wstrcpy(destinationPtrchunkOffset,sourcePtr,chunkLength);}}else{// 如果越界抛出异常thrownewArgumentOutOfRangeException(nameof(chunkLength),SR.ArgumentOutOfRange_Index);}}// 移动到前一个 chunk继续处理链表中的其他部分chunkchunk.m_ChunkPrevious;}while(chunk!null);// 直到没有更多 chunk 为止// 返回最终拼接好的字符串returnresult;}}}当调用sb.ToString()时会从最后一个 Chunk 开始沿着m_ChunkPrevious一路向前遍历把所有 Chunk 的字符数组拼接起来最终返回完整的字符串。这个过程虽然比单一块慢但比起频繁复制数组在大多数场景下是值得的。