Wie wird die StringBuilder-class implementiert? Werden bei jedem Anhängen intern neue String-Objekte erstellt?

Wie wird die StringBuilder-class implementiert? Werden bei jedem Anhängen intern neue String-Objekte erstellt?

   

In .NET 2.0 wird intern die String class verwendet. String ist nur außerhalb des System Namespace unveränderlich, daher kann StringBuilder dies tun.

In .NET 4.0 wurde String geändert, um char[] .

In 2.0 sah StringBuilder aus

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

Aber in 4.0 sieht es so aus:

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

So wurde offensichtlich von der Verwendung einer string zur Verwendung von char[] gewechselt.

BEARBEITEN: Aktualisierte Antwort, um Änderungen in .NET 4 (die ich gerade erst entdeckt habe) widerzuspiegeln.

Die angenommene Antwort verfehlt die Marke um eine Meile. Die wesentliche Änderung an StringBuilder in 4.0 ist nicht die Änderung von einer unsicheren string in char[] – es ist die Tatsache, dass StringBuilder jetzt tatsächlich eine verkettete Liste von StringBuilder Instanzen ist.


Der Grund für diese Änderung sollte offensichtlich sein: Jetzt ist es nie notwendig, den Puffer neu zuzuordnen (eine teure Operation, da neben dem Zuweisen von mehr Speicher auch der gesamte Inhalt aus dem alten Puffer in den neuen Puffer kopiert werden muss) .

Dies bedeutet, dass das Aufrufen von ToString() jetzt etwas langsamer ist, da die letzte Zeichenfolge berechnet werden muss, aber das Ausführen einer großen Anzahl von Append() jetzt wesentlich schneller ist. Dies entspricht dem typischen Anwendungsfall für StringBuilder : viele Aufrufe von Append() gefolgt von einem einzelnen Aufruf von ToString() .


Sie können hier Benchmarks finden . Das Fazit? Der neue StringBuilder mit verknüpfter Liste verwendet marginal mehr Speicher, ist aber für den typischen Anwendungsfall deutlich schneller.

Nicht wirklich – es verwendet internen Zeichenpuffer. Nur wenn die Pufferkapazität erschöpft ist, wird ein neuer Puffer zugewiesen. Append-Operation fügt einfach zu diesem Puffer hinzu, string-Objekt wird erstellt, wenn die ToString () – Methode aufgerufen wird – fortan ist es ratsam für viele String-Verkettungen, da jede traditionelle Zeichenkette concat op eine neue Zeichenkette erzeugen würde. Sie können auch die Anfangskapazität für den Zeichenfolgengenerator angeben, wenn Sie eine grobe Vorstellung davon haben, um Mehrfachzuweisungen zu vermeiden.

Edit : Leute weisen darauf hin, dass mein Verständnis falsch ist. Bitte ignoriere die Antwort (ich lösche sie lieber nicht – sie wird als Beweis meiner Unwissenheit stehen 🙂

Ich habe ein kleines Beispiel gemacht, um zu demonstrieren, wie StringBuilder in .NET 4 funktioniert. Der Vertrag ist

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

Und das ist eine sehr einfache Implementierung

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

Dieser Code ist nicht Thread-sicher, führt keine Eingabeüberprüfung durch und verwendet nicht die interne (unsichere) Magie von System.String. Es zeigt jedoch die Idee hinter der StringBuilder-class.

Einige Komponententests und vollständiger Beispielcode finden Sie unter github .

Wenn ich mir .NET Reflector bei .NET 2 anschaue, dann werde ich folgendes finden:

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

Es ist also eine mutierte String Instanz …

EDIT Außer in .NET 4 ist es ein char[]

Wenn Sie eine der möglichen Implementierungen sehen möchten (die der Version ähnelt, die mit der Microsoft-Implementierung bis Version 3.5 ausgeliefert wurde), können Sie die Quelle von Mono auf GitHub sehen.