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

前言

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

什么是StringBuilder

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

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

StringBuilder的成员

测试环境:Unity

StringBuilder sb = new StringBuilder();

我们可以查看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对象里

在这里插入图片描述
我们很容易看出来,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

在这里插入图片描述
在这里插入图片描述
记住此时: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;
}

关于Java的拓展

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

总结

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

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

  • 9
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
StringBuilder是Java中一个可变字符串类型,它的底层实现是一个数组,当我们往StringBuilder对象中添加字符串时,它会先判断数组是否已满,如果已满就会创建一个新的数组,并将原有的字符串复制到新的数组中,再添加新的字符串。所以StringBuilder对象可以动态的添加和删除字符串,而不需要每次都重新分配内存。 StringBuilder底层实现的具体原理如下: 1. StringBuilder内部维护了一个字符数组char[] value和一个int类型的count,用于记录当前字符串的长度。 2. 当我们向StringBuilder中添加字符串时,如果当前字符数组的长度不足以容纳新的字符串,StringBuilder会先判断是否需要扩容。扩容的规则是按照原数组长度的2倍进行扩容,如果还不够,则直接扩容到新的字符串所需的长度。 3. 如果需要扩容,则会创建一个新的字符数组,将原有的字符串复制到新数组中,并添加新的字符串。 4. 如果不需要扩容,则直接将新的字符串添加到字符数组的末尾,并更新count的值。 5. 当我们需要删除字符串时,StringBuilder会将要删除的字符串从字符数组中移除,并更新count的值。 6. 当我们需要获取StringBuilder对象中的字符串时,它会根据当前count的值,将字符数组中的字符串复制到一个新的字符串中,并返回给调用者。 综上,StringBuilder底层实现的可变字符串原理就是通过动态扩容和字符数组复制来实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

就一枚小白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值