¿Cómo se implementa la clase StringBuilder? ¿Crea internamente nuevos objetos de cadena cada vez que agregamos?

¿Cómo se implementa la clase StringBuilder? ¿Crea internamente nuevos objetos de cadena cada vez que agregamos?

En .NET 2.0 usa internamente la clase String . String solo es inmutable fuera del espacio de nombres del System , por lo que StringBuilder puede hacerlo.

En .NET 4.0 String se cambió para usar char[] .

En 2.0 StringBuilder veía así

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

Pero en 4.0 se ve así:

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

Así que evidentemente se cambió de usar una string a usar un char[] .

EDITAR: Respuesta actualizada para reflejar los cambios en .NET 4 (que acabo de descubrir).

La respuesta aceptada pierde la marca por una milla. El cambio significativo en StringBuilder en 4.0 no es el cambio de una string insegura a char[] : es el hecho de que StringBuilder ahora es una lista enlazada de instancias de StringBuilder .


El motivo de este cambio debería ser obvio: ahora nunca es necesario reasignar el búfer (una operación costosa, ya que, además de asignar más memoria, también debe copiar todo el contenido del búfer antiguo al nuevo) .

Esto significa que llamar a ToString() ahora es un poco más lento, ya que la cadena final necesita ser calculada, pero hacer una gran cantidad de operaciones de Append() ahora es mucho más rápido. Esto encaja con el típico caso de uso de StringBuilder : muchas llamadas a Append() , seguidas por una sola llamada a ToString() .


Puede encontrar puntos de referencia aquí . ¿La conclusión? La nueva lista enlazada StringBuilder usa marginalmente más memoria, pero es significativamente más rápida para el típico caso de uso.

En realidad, no, usa el buffer de caracteres interno. Solo cuando la capacidad del buffer se agote, asignará un nuevo buffer. La operación de agregar simplemente agregará a este búfer, el objeto de cadena se creará cuando se le llame al método ToString (); de ahí en adelante, es recomendable para muchas concatenaciones de cadenas ya que cada cadena tradicional de concaturas crearía una nueva cadena. También puede especificar la capacidad inicial del generador de cadenas si tiene una idea aproximada para evitar asignaciones múltiples.

Editar : Las personas señalan que mi comprensión es incorrecta. Por favor, ignore la respuesta (prefiero no eliminarla, será una prueba de mi ignorancia 🙂

He hecho una pequeña muestra para demostrar cómo funciona StringBuilder en .NET 4. El contrato es

 public interface ISimpleStringBuilder { ISimpleStringBuilder Append(string value); ISimpleStringBuilder Clear(); int Lenght { get; } int Capacity { get; } } 

Y esta es una implementación muy básica

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

Este código no es seguro para subprocesos, no realiza ninguna validación de entrada y no utiliza la magia interna (insegura) de System.String. Sin embargo, demuestra la idea detrás de la clase StringBuilder.

Algunas pruebas de unidad y un código de muestra completo se pueden encontrar en github .

Si miro .NET Reflector en .NET 2 entonces encontraré esto:

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

Entonces, es una instancia de cadena mutada …

EDITAR Excepto en .NET 4 es un char[]

Si desea ver una de las posibles implementaciones (que es similar a la enviada con la implementación de Microsoft hasta v3.5), podría ver la fuente del mono uno en github.