Как реализуется класс StringBuilder? Внутренне ли он создает новые строковые объекты каждый раз, когда мы добавляем?

Как реализуется класс StringBuilder? Внутренне ли он создает новые строковые объекты каждый раз, когда мы добавляем?

Ответ 1

В .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[].

EDIT: Обновлен ответ, чтобы отразить изменения в .NET 4 (которые я только что открыл).

Ответ 2

Принятый ответ пропускает отметку на милю. Значительное изменение StringBuilder в 4.0 не является изменением от небезопасного string до char[] - это тот факт, что StringBuilder теперь фактически является связанным списком экземпляров StringBuilder.


Причина этого изменения должна быть очевидной: теперь нет необходимости перераспределять буфер (дорогостоящая операция, поскольку наряду с распределением большего объема памяти вам также необходимо скопировать все содержимое из старого буфера в новое один).

Это означает, что вызов ToString() теперь немного медленнее, так как окончательная строка должна быть вычислена, но выполнение большого числа операций Append() теперь значительно быстрее. Это соответствует типичному случаю использования StringBuilder: много вызовов Append(), за которым следует один вызов ToString().


Здесь вы можете найти эталоны здесь. Вывод? Новый связанный список StringBuilder использует немного больше памяти, но значительно быстрее для типичного варианта использования.

Ответ 3

Не совсем - он использует внутренний буфер символов. Только когда емкость буфера будет исчерпана, он будет выделять новый буфер. Операция добавления будет просто добавлена ​​в этот буфер, строковый объект будет создан, когда на него вызывается метод ToString(). В дальнейшем его рекомендуется для многих конкатенаций строк, поскольку каждая традиционная строка concat op создаст новую строку. Вы также можете указать начальную емкость строкового построителя, если у вас есть общее представление об этом, чтобы избежать множественных распределений.

Изменить. Люди указывают, что мое понимание ошибочно. Пожалуйста, проигнорируйте ответ (я скорее не удалю его - это станет доказательством моего невежества: -)

Ответ 4

Если я посмотрю на .NET Reflector на .NET 2, я найду это:

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;
}

Итак, это мутированный экземпляр строки...

EDIT За исключением .NET 4 это char[]

Ответ 5

Если вы хотите увидеть одну из возможных реализаций (это похоже на ту, что поставляется с реализацией microsoft до версии 3.5), вы можете увидеть источник Mono one на github.

Ответ 6

Я сделал небольшой образец, чтобы продемонстрировать, как работает 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.