StringBuilder在调用ToString后变得不可变吗?

时间:2010-11-12 15:30:00

标签: .net immutability tostring stringbuilder copy-on-write

我清楚地记得从早期的.NET开始,在StringBuilder上调用ToString用于提供新的字符串对象(要返回)与StringBuilder使用的内部char缓冲区。这样,如果使用StringBuilder构造了一个巨大的字符串,则调用ToString不必复制它。

在这样做时,StringBuilder必须阻止对缓冲区的任何其他更改,因为它现在由不可变字符串使用。因此,StringBuilder将切换到“copy-on-change”,其中任何尝试的更改将首先创建一个新的缓冲区,将旧缓冲区的内容复制到它,然后才更改它。

我认为假设StringBuilder将用于构造字符串,然后转换为常规字符串并丢弃。对我来说似乎是一个合理的假设。

现在就是这样。我在文档中找不到任何提及。但我不确定它是否有记录。

所以我使用Reflector(.NET 4.0)查看了ToString的实现,在我看来它实际上是复制字符串,而不是仅仅共享缓冲区:

[SecuritySafeCritical]
public override unsafe string ToString()
{
    string str = string.FastAllocateString(this.Length);
    StringBuilder chunkPrevious = this;
    fixed (char* str2 = ((char*) str))
    {
        char* chPtr = str2;
        do
        {
            if (chunkPrevious.m_ChunkLength > 0)
            {
                char[] chunkChars = chunkPrevious.m_ChunkChars;
                int chunkOffset = chunkPrevious.m_ChunkOffset;
                int chunkLength = chunkPrevious.m_ChunkLength;
                if ((((ulong) (chunkLength + chunkOffset)) > str.Length) ||     (chunkLength > chunkChars.Length))
                {
                    throw new ArgumentOutOfRangeException("chunkLength",     Environment.GetResourceString("ArgumentOutOfRange_Index"));
                }
                fixed (char* chRef = chunkChars)
                {
                    string.wstrcpy(chPtr + chunkOffset, chRef, chunkLength);
                }
            }
            chunkPrevious = chunkPrevious.m_ChunkPrevious;
        }
        while (chunkPrevious != null);
    }
    return str;
}

现在,正如我之前提到的,我清楚地记得在早期的情况下读到这种情况,如果是.NET。我甚至在此book中找到了提及。

我的问题是,这种行为被删除了吗?如果是这样,有谁知道为什么?这对我来说很有意义......

5 个答案:

答案 0 :(得分:5)

是的,你没记错的。 StringBuilder.ToString方法用于将内部缓冲区作为字符串返回,并将其标记为已使用,以便StringBuilder的其他更改必须分配新缓冲区。

由于这是一个实现细节,因此文档中未提及。这就是为什么他们可以在不破坏类定义行为的任​​何内容的情况下更改底层实现。

从发布的代码中可以看出,不再有单个内部缓冲区,而是将字符存储在块中,ToString方法将块组合成一个字符串。

实施中这种变化的原因很可能是他们收集了有关StringBuilder类实际使用方式的信息,并得出结论:这种方法在平均情况和最差情况之间给出了更好的性能。

答案 1 :(得分:5)

是的,这已经完全重新设计用于.NET 4.0。它现在使用一根绳子,一个字符串构建器的链表来存储不断增长的内部缓冲区。当您无法正确猜出初始容量且文本量很大时,这是一个问题的解决方法。这会创建大量未使用的内部缓冲区副本,从而堵塞大对象堆。来自参考源的源代码中的此注释是相关的:

    // We want to keep chunk arrays out of large object heap (< 85K bytes ~ 40K chars) to be sure.
    // Making the maximum chunk size big means less allocation code called, but also more waste 
    // in unused characters and slower inserts / replaces (since you do need to slide characters over
    // within a buffer).
    internal const int MaxChunkSize = 8000;

答案 2 :(得分:2)

以下是Reflector中的StringBuilder.ToString的.NET 1.1实现:

public override string ToString()
{
    string stringValue = this.m_StringValue;
    int currentThread = this.m_currentThread;
    if ((currentThread != 0) && (currentThread != InternalGetCurrentThread()))
    {
        return string.InternalCopy(stringValue);
    }
    if ((2 * stringValue.Length) < stringValue.ArrayLength)
    {
        return string.InternalCopy(stringValue);
    }
    stringValue.ClearPostNullChar();
    this.m_currentThread = 0;
    return stringValue;
}

据我所知,在某些情况下它会返回字符串而不复制它。但是,我不认为StringBuilder变得不可变。相反,如果您继续写入StringBuilder,我认为它将使用copy-on-write。

答案 3 :(得分:0)

这很可能只是一个实现细节,而不是StringBuilder.ToString提供的界面上的文档约束。您不确定是否曾记录过这一事实可能表明情况确实如此。

书籍通常会详细说明实施,以展示如何使用某些内容的一些见解,但大多数都会发出警告,说明实施可能会发生变化。

一个很好的例子,说明为什么不应该依赖实现细节。

我怀疑构建器变得不可变不是一个特性,而只是ToString实现的副作用。

答案 4 :(得分:0)

我以前没见过,所以我的猜测是:StringBuilder的内部存储看起来不再是简单的string,而是一组“块”。 ToString无法返回对此内部字符串的引用,因为它不再存在。

(版本4.0 StringBuilders现在是ropes吗?)