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


    前言

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

    什么是StringBuilder

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

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

    StringBuilder的成员

    测试环境:Unity

    StringBuilder sb = new StringBuilder();
    
    • 1

    我们可以查看StringBuilder的内部结构
    在这里插入图片描述
    我们目前只需要知道几个常用的成员就行了

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

    StringBuilder增加元素原理

    sb.Append(1); // 将 1 元素加入到StringBuilder对象里
    sb.Append(2); // 将 2 元素加入到StringBuilder对象里
    
    • 1
    • 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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Capacity:1,元素数量:0

    在这里插入图片描述
    在这里插入图片描述
    记住此时:m_ChunkPrevious 对象为null

    Capacity:1,元素数量:1

    在这里插入图片描述
    在这里插入图片描述

    Capacity:2,元素数量:2

    在这里插入图片描述

    注意!

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

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

    我们画下图吧

    在这里插入图片描述

    我们继续增加元素

    Capacity:4,元素数量:3

    在这里插入图片描述
    由于元素数量 = 3 > Capacity = 2,所以再次发生扩容,Capacity = 4

    我们也能够发现,生成了一个新的StringBuilder对象,原来元素为1的数组,变成了新的StringBuilder对象的 m_ChunkPrevious 对象
    在这里插入图片描述
    我们现在应该很清楚了,StringBuilder底层其实是数组存储元素,链表处理扩容,并且是头插法

    Capacity:4,元素数量:4

    在这里插入图片描述
    在这里插入图片描述

    Capacity:8,元素数量:5

    在这里插入图片描述
    由于元素数量 = 5 > Capacity = 4,所以再次发生扩容,Capacity = 8
    在这里插入图片描述

    StringBuilder底层总结

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

    关于Java的拓展

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

    所以C#底层应该是这样子的结构
    在这里插入图片描述

    使用解读

    方法意义
    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方法
    由于我们是头插法处理链表,也就是我们需要倒序遍历链表
    比如我们上一个例子
    在这里插入图片描述
    我们想调用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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    关于Java的拓展

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

    总结

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

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

  • 相关阅读:
    二维码智慧门牌管理系统升级解决方案:采集项目的建立与运用
    【JavaScript】案例2:轮播图
    [力扣146. LRU 缓存 ](https://leetcode.cn/problems/lru-cache/description/)
    莫队
    FTP客户端lftp
    初识网络之http协议
    安全漏洞-linux漏洞修复命令
    torch中tensor的相关操作
    前端数据库大批量存,indexdDB使用
    图文多模态模型CLIP
  • 原文地址:https://blog.csdn.net/qq_52855744/article/details/126802261