15

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

在这样做时,StringBuilder 必须防止对缓冲区进行任何额外的更改,因为它现在被一个不可变的字符串使用。结果,StringBuilder 将切换到“更改时复制”,其中任何尝试的更改都将首先创建一个新缓冲区,将旧缓冲区的内容复制到它,然后才对其进行更改。

我认为假设是 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 就是这种情况。我什至在这本书中找到了提及。

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

4

5 回答 5

5

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

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

正如您从发布的代码中看到的那样,不再有单个内部缓冲区,而是将字符存储在块中,并且该ToString方法将块放在一起形成一个字符串。

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

于 2010-11-12T15:42:33.753 回答
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;
于 2010-11-12T16:33:51.667 回答
2

这是StringBuilder.ToStringReflector 的 .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.

于 2010-11-12T15:46:39.857 回答
0

这很可能只是一个实现细节,而不是对StringBuilder.ToString. 您不确定它是否被记录在案的事实可能表明情况就是如此。

书籍通常会详细介绍实现,以展示对如何使用某些东西的一些见解,但大多数都带有一个警告,即实现可能会发生变化。

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

我怀疑让构建器变得不可变并不是一个特性,而仅仅是实现ToString.

于 2010-11-12T15:39:13.113 回答
0

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

(4.0 版 StringBuilders 现在是绳子吗?)

于 2010-11-12T15:40:16.857 回答