StringBuilder类是如何实现的?每次追加时它是否在内部创建新的字符串对象?

问题描述 投票:50回答:6

StringBuilder类是如何实现的?每次追加时它是否在内部创建新的字符串对象?

c# .net string stringbuilder
6个回答
53
投票

在.NET 2.0中,它在内部使用String类。 String只是在System命名空间之外不可变,所以StringBuilder可以做到这一点。

在.NET 4.0中,String更改为使用char[]

在2.0 StringBuilder看起来像这样

public sealed class StringBuilder : ISerializable
{
    // Fields
    private const string CapacityField = "Capacity";
    internal const int DefaultCapacity = 0x10;
    internal IntPtr m_currentThread;
    internal int m_MaxCapacity;
    internal volatile string m_StringValue; // HERE ----------------------
    private const string MaxCapacityField = "m_MaxCapacity";
    private const string StringValueField = "m_StringValue";
    private const string ThreadIDField = "m_currentThread";

但在4.0中它看起来像这样:

public sealed class StringBuilder : ISerializable
{
    // Fields
    private const string CapacityField = "Capacity";
    internal const int DefaultCapacity = 0x10;
    internal char[] m_ChunkChars; // HERE --------------------------------
    internal int m_ChunkLength;
    internal int m_ChunkOffset;
    internal StringBuilder m_ChunkPrevious;
    internal int m_MaxCapacity;
    private const string MaxCapacityField = "m_MaxCapacity";
    internal const int MaxChunkSize = 0x1f40;
    private const string StringValueField = "m_StringValue";
    private const string ThreadIDField = "m_currentThread";

显然,它从使用string变为使用char[]

编辑:更新了答案,以反映.NET 4中的变化(我刚才发现)。


28
投票

接受的答案错过了一英里的标记。 4.0中StringBuilder的重大变化不是从不安全的stringchar[]的变化 - 事实上StringBuilder现在实际上是StringBuilder实例的链接列表。


这种改变的原因应该是显而易见的:现在永远不需要重新分配缓冲区(这是一项昂贵的操作,因为在分配更多内存的同时,您还必须将旧缓冲区中的所有内容复制到新缓冲区中)。

这意味着调用ToString()现在稍微慢了,因为需要计算最终的字符串,但是现在执行大量的Append()操作要快得多。这符合StringBuilder的典型用例:很多调用Append(),然后调用ToString()


你可以找到基准here。结论?新的链接列表StringBuilder使用了更多的内存,但对于典型的用例来说明显更快。


7
投票

不是 - 它使用内部字符缓冲区。只有当缓冲区容量耗尽时,它才会分配新的缓冲区。追加操作将简单地添加到此缓冲区,当调用ToString()方法时将创建字符串对象 - 此后,由于每个传统的字符串连接将创建新字符串,因此它适用于许多字符串连接。您还可以指定字符串构建器的初始容量,如果您对它有粗略的了解以避免多次分配。

编辑:人们指出我的理解是错误的。请忽略答案(我宁愿不删除它 - 它将作为我无知的证据:-)


3
投票

我做了一个小样本来演示StringBuilder在.NET 4中是如何工作的。合同是

public interface ISimpleStringBuilder
{
    ISimpleStringBuilder Append(string value);
    ISimpleStringBuilder Clear();
    int Lenght { get; }
    int Capacity { get; }
}

这是一个非常基本的实现

public class SimpleStringBuilder : ISimpleStringBuilder
{
    public const int DefaultCapacity = 32;

    private char[] _internalBuffer;

    public int Lenght { get; private set; }
    public int Capacity { get; private set; }

    public SimpleStringBuilder(int capacity)
    {
        Capacity = capacity;
        _internalBuffer = new char[capacity];
        Lenght = 0;
    }

    public SimpleStringBuilder() : this(DefaultCapacity) { }

    public ISimpleStringBuilder Append(string value)
    {
        char[] data = value.ToCharArray();

        //check if space is available for additional data
        InternalEnsureCapacity(data.Length);

        foreach (char t in data)
        {
            _internalBuffer[Lenght] = t;
            Lenght++;
        }

        return this;
    }

    public ISimpleStringBuilder Clear()
    {
        _internalBuffer = new char[Capacity];
        Lenght = 0;
        return this;
    }

    public override string ToString()
    {
        //use only non-null ('\0') characters
        var tmp = new char[Lenght];
        for (int i = 0; i < Lenght; i++)
        {
            tmp[i] = _internalBuffer[i];
        }
        return new string(tmp);
    }

    private void InternalExpandBuffer()
    {
        //double capacity by default
        Capacity *= 2;

        //copy to new array
        var tmpBuffer = new char[Capacity];
        for (int i = 0; i < _internalBuffer.Length; i++)
        {
            char c = _internalBuffer[i];
            tmpBuffer[i] = c;
        }
        _internalBuffer = tmpBuffer;
    }

    private void InternalEnsureCapacity(int additionalLenghtRequired)
    {
        while (Lenght + additionalLenghtRequired > Capacity)
        {
            //not enough space in the current buffer    
            //double capacity
            InternalExpandBuffer();
        }
    }
}

此代码不是线程安全的,不进行任何输入验证,也不使用System.String的内部(不安全)魔法。然而它确实展示了StringBuilder类背后的想法。

有些单元测试和完整的示例代码可以在github找到。


2
投票

如果我在.NET 2中查看.NET Reflector,那么我会发现:

public StringBuilder Append(string value)
{
    if (value != null)
    {
        string stringValue = this.m_StringValue;
        IntPtr currentThread = Thread.InternalGetCurrentThread();
        if (this.m_currentThread != currentThread)
        {
            stringValue = string.GetStringForStringBuilder(stringValue, stringValue.Capacity);
        }
        int length = stringValue.Length;
        int requiredLength = length + value.Length;
        if (this.NeedsAllocation(stringValue, requiredLength))
        {
            string newString = this.GetNewString(stringValue, requiredLength);
            newString.AppendInPlace(value, length);
            this.ReplaceString(currentThread, newString);
        }
        else
        {
            stringValue.AppendInPlace(value, length);
            this.ReplaceString(currentThread, stringValue);
        }
    }
    return this;
}

所以它是一个变异的字符串实例......

编辑除了在.NET 4中它是一个char[]


2
投票

如果你想看到一个可能的实现(这类似于微软实现到v3.5的那个),你可以在github上看到the source of the Mono one

© www.soinside.com 2019 - 2024. All rights reserved.