StringBuilder становится неизменным после вызова ToString?
Я отчетливо помню из первых дней .NET, что вызов ToString в StringBuilder используется для предоставления нового строкового объекта (который будет возвращен) с внутренним буфером char, используемым StringBuilder. Таким образом, если вы построили огромную строку с помощью StringBuilder, вызов ToString не должен был ее копировать.
При этом StringBuilder должен был предотвратить любые дополнительные изменения в буфере, потому что теперь он использовался неизменяемой строкой. В результате StringBuilder переключится на "copy-on-change", сделанный там, где любые попытки изменения сначала создадут новый буфер, скопируют содержимое старого буфера и только затем изменят его.
Я думаю, что предположение состояло в том, что StringBuilder будет использоваться для построения строки, затем преобразовывается в обычную строку и отбрасывается. Мне кажется разумным предположением.
Теперь вот что. Я не могу найти упоминания об этом в документации. Но я не уверен, что это когда-либо документировалось.
Итак, я посмотрел на реализацию ToString с использованием Reflector (.NET 4.0), и мне кажется, что он фактически копирует строку, а не просто передает буфер:
[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.
Мой вопрос: было ли это поведение отменено? Если да, то кто-нибудь знает почему? Это имело для меня смысл...
Ответы
Ответ 1
Yup, это было полностью переработано для .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
Да, вы правильно помните. Метод StringBuilder.ToString
, используемый для возврата внутреннего буфера в качестве строки, и помечает его как используемого, так что дополнительные изменения в StringBuilder
должны были выделять новый буфер.
Поскольку это деталь реализации, она не упоминается в документации. Вот почему они могут изменить базовую реализацию, не нарушая ничего в определенном поведении класса.
Как вы видите из выложенного кода, нет никакого внутреннего внутреннего буфера, вместо этого символы хранятся в кусках, а метод ToString
помещает куски в строку.
Причиной такого изменения в реализации является то, что они собрали информацию о том, как действительно используется класс StringBuilder
, и приходят к выводу, что этот подход дает лучшую производительность, взвешенную между средними и худшими ситуациями.
Ответ 3
Вот реализация .NET 1.1 StringBuilder.ToString
из Reflector:
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
становится неизменным. Вместо этого я думаю, что он будет использовать copy-on-write, если вы продолжаете писать в StringBuilder
.
Ответ 4
Это скорее всего была просто деталью реализации, а не документированным ограничением на интерфейс, предоставляемый StringBuilder.ToString
. Тот факт, что вы не уверены в том, что он когда-либо был задокументирован, может предположить, что это так.
Книги часто описывают реализации, чтобы показать некоторое понимание того, как их использовать, но большинство из них несут предупреждение о том, что реализация может быть изменена.
Хороший пример того, почему никогда не следует полагаться на детали реализации.
Я подозреваю, что это не была особенность, когда строитель стал неизменным, а просто побочным эффектом реализации ToString
.
Ответ 5
Я раньше этого не видел, поэтому я предполагаю, что внутреннее хранилище StringBuilder
кажется уже не простым string
, а набором "кусков". ToString
не может вернуть ссылку на эту внутреннюю строку, потому что она больше не существует.
(Есть версии 4.0 StringBuilders теперь канаты?)