Come viene implementata la class StringBuilder? Crea internamente nuovi oggetti stringa ogni volta che aggiungiamo?

Come viene implementata la class StringBuilder? Crea internamente nuovi oggetti stringa ogni volta che aggiungiamo?

In .NET 2.0 utilizza internamente la class String . String è solo immutabile al di fuori dello spazio System nomi di System , quindi StringBuilder può farlo.

In .NET 4.0 String stato modificato per utilizzare char[] .

In 2.0 StringBuilder sembrava così

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

Ma in 4.0 sembra così:

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

Quindi evidentemente è stato cambiato dall’uso di una string all’utilizzo di un char[] .

EDIT: risposta aggiornata per riflettere i cambiamenti in .NET 4 (che ho appena scoperto).

La risposta accettata manca il marchio di un miglio. La modifica significativa a StringBuilder in 4.0 non è la modifica da una string non sicura a char[] – è il fatto che StringBuilder ora è in realtà un elenco collegato di istanze StringBuilder .


La ragione di questo cambiamento dovrebbe essere ovvia: ora non c’è mai bisogno di riallocare il buffer (un’operazione costosa, dato che, oltre a allocare più memoria, è necessario copiare tutti i contenuti dal vecchio buffer a quello nuovo) .

Ciò significa che chiamare ToString() ora è leggermente più lento, poiché la stringa finale deve essere calcasting, ma eseguire un numero elevato di operazioni Append() ora è notevolmente più veloce. Questo si adatta al tipico caso d’uso di StringBuilder : molte chiamate ad Append() , seguite da una singola chiamata a ToString() .


Puoi trovare benchmark qui . La conclusione? La nuova lista collegata StringBuilder utilizza marginalmente più memoria, ma è significativamente più veloce per il tipico caso d’uso.

Non proprio: utilizza il buffer dei caratteri interni. Solo quando la capacità del buffer si esaurisce, allocherà un nuovo buffer. L’operazione di aggiunta verrà semplicemente aggiunta a questo buffer, l’object stringa verrà creato quando il metodo ToString () viene chiamato su di esso; d’ora in poi, è consigliabile per molte concatenazioni di stringhe poiché ogni concatop tradizionale stringa creerebbe una nuova stringa. È anche ansible specificare la capacità iniziale del generatore di stringhe se si ha una vaga idea su di esso per evitare allocazioni multiple.

Modifica : le persone sottolineano che la mia comprensione è sbagliata. Si prega di ignorare la risposta (preferisco non cancellarla – sarà una prova della mia ignoranza 🙂

Ho fatto un piccolo esempio per dimostrare come funziona StringBuilder in .NET 4. Il contratto è

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

E questa è un’implementazione molto semplice

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

Questo codice non è thread-safe, non esegue alcuna convalida dell’input e non utilizza la magia interna (non sicura) di System.String. Tuttavia dimostra l’idea alla base della class StringBuilder.

Alcuni test unitari e un codice di esempio completo possono essere trovati su github .

Se guardo .NET Reflector su .NET 2, troverò questo:

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

Quindi è un’istanza di stringa mutata …

EDIT Tranne in .NET 4 è un char[]

Se vuoi vedere una delle possibili implementazioni (che è simile a quella fornita con l’implementazione microsoft fino alla v3.5) potresti vedere l’origine di Mono su github.