C# StringBuilder 底层深入原理分析以及使用详解

目录

    • 前言
    • 什么是StringBuilder
    • StringBuilder的成员
    • StringBuilder增加元素原理
    • StringBuilder扩容原理
      • Capacity:1,元素数量:0
      • Capacity:1,元素数量:1
      • Capacity:2,元素数量:2
      • Capacity:4,元素数量:3
      • Capacity:4,元素数量:4
      • Capacity:8,元素数量:5
    • StringBuilder底层总结
      • 关于Java的拓展
    • 使用解读
    • ToString方法解析
      • 关于Java的拓展
    • 总结

前言

最近在研究string的时候,发现StringBuilder底层实现原理很有意思,故作记录。

什么是StringBuilder

  1. StringBuilder是一个类,是System.Text命名空间下的一个类。
  2. StringBuilder主要用于处理字符串拼接。

由于string的不可变性,导致每一次做字符串拼接的时候,都会在托管堆上new一个新的字符串,也就是说它会造成GC
而字符串拼接可以说是非常常用的,所以为了避免造成过多的GC,可以使用StringBuilder先进行字符串拼接,再使用ToString方法转换为字符串。

StringBuilder的成员

测试环境:Unity

StringBuilder sb = new StringBuilder();

我们可以查看StringBuilder的内部结构
C# StringBuilder 底层深入原理分析以及使用详解_第1张图片
我们目前只需要知道几个常用的成员就行了

成员 意义
Capacity 字符数组m_ChunkChars的最大容量
Length 当前StringBuilder对象实际管理的字符串长度
m_ChunkChars 保存StringBuilder所管理着的字符串中的字符
m_ChunkOffset 字符定位的偏移量
m_ChunkPrevious 指向上一个StringBuilder对象

StringBuilder增加元素原理

sb.Append(1); // 将 1 元素加入到StringBuilder对象里
sb.Append(2); // 将 2 元素加入到StringBuilder对象里

C# StringBuilder 底层深入原理分析以及使用详解_第2张图片
我们很容易看出来,StringBuilder底层其实是管理着一个char数组
当我们使用Append方法向StringBuilder中添加元素时,发现它是向字符数组中添加元素

StringBuilder扩容原理

当我们字符数组存不下元素的时候,也就是元素个数大于Capacity的时候,StringBuilder就会触发扩容机制

为了方便,我们在定义StringBuilder对象的时候,指定Capacity为1

StringBuilder sb = new StringBuilder(1);

for(int cnt = 0; cnt <= 5; cnt++) {
	sb.Append(cnt);
}

Capacity:1,元素数量:0

C# StringBuilder 底层深入原理分析以及使用详解_第3张图片
C# StringBuilder 底层深入原理分析以及使用详解_第4张图片
记住此时:m_ChunkPrevious 对象为null

Capacity:1,元素数量:1

C# StringBuilder 底层深入原理分析以及使用详解_第5张图片
在这里插入图片描述

Capacity:2,元素数量:2

C# StringBuilder 底层深入原理分析以及使用详解_第6张图片

注意!

我们发现原本Capacity = 1的时候,想要再加入元素1的时候,容量已经不够了,所以这里发生了扩容,并且Capacity是扩大了一倍,也就是变为原来的两倍了

而且我们发现 m_ChunkPrevious 对象不为null了
更仔细一点,我们会发现,m_ChunkPrevious对象中的字符数组,存的数组是我们还没有添加元素为1时候的数组

我们画下图吧

C# StringBuilder 底层深入原理分析以及使用详解_第7张图片

我们继续增加元素

Capacity:4,元素数量:3

C# StringBuilder 底层深入原理分析以及使用详解_第8张图片
由于元素数量 = 3 > Capacity = 2,所以再次发生扩容,Capacity = 4

我们也能够发现,生成了一个新的StringBuilder对象,原来元素为1的数组,变成了新的StringBuilder对象的 m_ChunkPrevious 对象
C# StringBuilder 底层深入原理分析以及使用详解_第9张图片
我们现在应该很清楚了,StringBuilder底层其实是数组存储元素,链表处理扩容,并且是头插法

Capacity:4,元素数量:4

C# StringBuilder 底层深入原理分析以及使用详解_第10张图片
C# StringBuilder 底层深入原理分析以及使用详解_第11张图片

Capacity:8,元素数量:5

C# StringBuilder 底层深入原理分析以及使用详解_第12张图片
由于元素数量 = 5 > Capacity = 4,所以再次发生扩容,Capacity = 8
C# StringBuilder 底层深入原理分析以及使用详解_第13张图片

StringBuilder底层总结

数组存储元素,链表处理扩容

关于Java的拓展

C#和Java的处理不一样,底层都是由数组存储,但是Java扩容是直接新建一个数组,大小为原来的两倍。这里我认为关于扩容方面,C#处理的比Java更好,原因是Java是重新开辟两倍原来的Capacity大小的数组,而C#只开辟一倍原来的Capacity大小的数组,而且Java要把原来的元素完全复制过来,而C#不需要

所以C#底层应该是这样子的结构
C# StringBuilder 底层深入原理分析以及使用详解_第14张图片

使用解读

方法 意义
Append方法及重载 添加元素
Insert方法及重载 向指定位置插入元素
Replace方法及重载 使用新元素替换老元素
Remove方法 从指定索引位移除指定数量的字符,它没有重载。方法Insert、Replace和Remove都是对内部字符数组m_ChunkChar和链表中m_ChunkPrevious内的字符数组m_ChunkChar操作,StringBuilder内部实现有点“绕”,感兴趣的可以自行去研究
ToString方法 StringBuilder重写了基类Object的ToString()方法用来获取StringBuilder对象的字符串表示,它是将链表m_ChunkPrevious中的字符数组m_ChunkChars及当前StringBuilder对象的字符数组m_ChunkChar中的字符转成String对象返回,这一步是创建一个新的String对象,所以对这个String对象(ToString()的结果)的操作不会影响到StringBuilder对象内部的字符

ToString方法解析

需要着重注意下ToString方法
由于我们是头插法处理链表,也就是我们需要倒序遍历链表
比如我们上一个例子
C# StringBuilder 底层深入原理分析以及使用详解_第15张图片
我们想调用StringBuilder对象的ToString方法
输出的应该是:01234

我们来看看源码

// 核心步骤一,长度相关:
public int Length
{
    [__DynamicallyInvokable]
    get
    {
        /******** 新建stringBuilder时,会有两个参数(offset+数组长度)*********/
        return m_ChunkOffset + m_ChunkLength;
    }
}

public unsafe override string ToString()
{
	if (Length == 0)
    {
        return string.Empty;
    }

    // 新开辟一个新的数组空间
    // FastAllocateString函数负责分配长度为Length的空字符串
    string text = string.FastAllocateString(Length);
    StringBuilder stringBuilder = this;

    // fixed 使用指针的关键字
    fixed (char* ptr = text) // 新开辟空间的数组,堆地址赋给指针变量ptr
    {
        // 整个 do-while 倒序遍历单向链表 
        // 顺序:4 -> 23 -> 1 -> 0
        do
        {
            if (stringBuilder.m_ChunkLength > 0)
            {
                char[] chunkChars = stringBuilder.m_ChunkChars;
                int chunkOffset = stringBuilder.m_ChunkOffset;
                int chunkLength = stringBuilder.m_ChunkLength;

                // 长度超出了int最大值或者大于新开辟空间的长度
                // 例如数组长度刚刚好是int最大值,这个后Append两个字符
                if ((uint)(chunkLength + chunkOffset) > text.Length
                              ||
                          (uint)chunkLength > (uint)chunkChars.Length)
                {
                    throw new ArgumentOutOfRangeException("chunkLength", Environment.GetResourceString("ArgumentOutOfRange_Index"));
                }

                // 当前stringBuilder它的char[]的指针,堆地址赋给指针变量smem
                fixed (char* smem = chunkChars)
                {
                    // CLR公共语言运行时 原生提供(copy),将当前char[]元素克隆到新开辟的空间去
                    // ptr + chunkOffset:ptr指的是开头,第三个char[]是放在后面位置的,所以要添加偏移量offset
                    // smem:当前char[]数组指针(引用地址)
                    // chunkLength: 当前char[]数组被使用的长度(被占用)
                    string.wstrcpy(ptr + chunkOffset, smem, chunkLength);
                }
            }
            // stringBuilder = 上一个stringBuilder(也就是第二个stringBuilder)
            stringBuilder = stringBuilder.m_ChunkPrevious;
        }
        while (stringBuilder != null);
    }
    // 最后都添加到最初新开辟的空间数组里去:text
    return text;
}

关于Java的拓展

由于Java底层就是一个字符数组,扩容不像C#用链表处理
所以在ToString的时候,Java可以非常容易的new一个字符串,然后把字符数组复制到新的字符串中,然后返回
所以不能说C#一定处理的比Java好,各有优势,毕竟大家都发展这么久了,肯定都有自己的理由

总结

  1. StringBuilder底层是字符数组
  2. 扩容使用链表处理,并且是头插法

PS:尾插法不是更容易理解和处理吗?难道有什么特殊处理?

你可能感兴趣的:(C#,c#,开发语言)