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

53

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


3
我从这个问题中也学到了新东西。 - Brian Rasmussen
1
@Brian Rasmussen 等待 Jon Skeet 的回答。我敢打赌它会很大,充满了新的学习内容 ;) - prostynick
只是猜测。它被分块以避免大字符串的 LOH。 - user359024
6个回答

55

.NET 2.0内部使用String类。在System命名空间之外,String是不可变的,因此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中的更改(我刚刚发现)。


毫无头绪..我想我要使用反射魔法来满足我的好奇心 :) - cwap
据我所知,在.NET 4中,它内部保存的是Char数组,而不是String。 (也许这已经改变了?)@Brian - Fredrik Mörk
4
@Marc:这让我很好奇,所以我用 Reflector 检查了一下;看起来这个东西已经改变了。之前它是一个 string,现在似乎是在操作一个 char 数组。 - Fredrik Mörk
@Fredrik - 那我收回之前的话! - Marc Gravell
1
http://www.nesterovsky-bros.com/weblog/2010/08/25/StringAndStringBuilderInNET4.aspx - user195488
显示剩余3条评论

35

接受的答案完全错了。StringBuilder在4.0中的重大改变不是从不安全的string类型到char[]类型的转变,而是事实上它现在是一个链表的StringBuilder实例。


这个改变的原因很明显:现在再也没有必要重新分配缓冲区(一项昂贵的操作,因为除了分配更多的内存外,您还必须将所有内容从旧缓冲区复制到新缓冲区)。

这意味着调用ToString()现在会稍微慢一些,因为最终字符串需要计算,但进行大量的Append()操作现在会显着快得多。这与StringBuilder的典型用例相符:大量调用Append(),然后跟随一个单独的调用ToString()。


您可以在这里找到基准测试结果。结论是,新的链表StringBuilder使用的内存略微增加,但对于典型用例来说速度显着更快。


7
并不是这样 - 它使用内部字符缓冲区。只有在缓冲区容量耗尽时,它才会分配新的缓冲区。附加操作将简单地添加到此缓冲区中,当调用ToString()方法时,字符串对象将被创建 - 因此,建议对许多字符串连接进行优化,因为每个传统的字符串连接操作都会创建新的字符串。如果您对其有大致的想法以避免多次分配,则还可以指定字符串构建器的初始容量。

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


1
它表现得就像是一个字符缓冲区,但实际上它是一个变异的string实例。真的。 - Marc Gravell
谢谢Marc - 我的印象是它使用字符缓冲区。这意味着它将具有一些本地实现来改变字符串对象。 - VinayC
当然可以,但这是一个核心框架类。它能够访问本地实现。 - Marc Gravell
1
抱歉,看起来(此页面上的先前评论)在.NET 4中已经发生了变化。 - Marc Gravell
不用在意 - 我之前也有同样的印象,甚至是在之前的版本中 - 如此确定以至于我甚至没有通过反射器检查它。 - VinayC
1
无论如何,让我们明确一点,目前这个答案是.NET 4.0中唯一正确的答案,它不应该被贬低为“不正确”。其他一些答案已经发现了char[],但如果没有注意到绳索结构(m_ChunkPrevious),那就是一个无用的事实,而这才是真正的新东西。 - Jirka Hanika

5

我制作了一个小示例,演示了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上查看Mono版本的源代码链接


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接