Strings sind in C# unveränderlich: s += "x" verlängert s nicht, sondern erzeugt einen neuen String mit den alten Zeichen plus den neuen, und der alte String wird zu Müll. Einmal ist das nichts. In einer Schleife, die tausende Male läuft, kopiert jeder Durchlauf alles bisher Gebaute, die Gesamtarbeit wächst also mit dem Quadrat der Länge.
StringBuilder aus System.Text ist ein veränderlicher Puffer. Append schreibt in freien Platz am Ende und vergrößert den Puffer gelegentlich, und ToString() erzeugt am Ende einen einzigen String.
Ausgabe:
Order #1042, total 59.97
Status: shipped
**********
51
Warum Verkettung in einer Schleife langsam ist
Hier wird derselbe Text auf beide Arten gebaut. Die Zeiten hängen vom Rechner ab, die Zahlen unten sind also nur ein Beispiel; entscheidend ist der Abstand, und der wächst mit der Schleife:
Beispielausgabe:
string += 482 ms, length 188890
StringBuilder 1 ms, length 188890
Bei 20.000 Anhängevorgängen kopiert die Version mit += insgesamt fast 2 Milliarden Zeichen. StringBuilder kopiert jedes Zeichen etwa zweimal, einmal beim Anhängen und noch einmal in ToString(), deshalb bleibt er bei rund einer Millisekunde.
Append, AppendLine und AppendFormat
Append hat Überladungen für jeden eingebauten Typ und gibt denselben Builder zurück, Aufrufe lassen sich also verketten. AppendLine fügt nach seinem Argument einen Zeilenumbruch an. AppendFormat nimmt einen kombinierten Formatstring wie string.Format:
Ausgabe:
RECEIPT
-------
Espresso 2.40
Croissant 3.10
Orange juice 4.50
TOTAL 10.00
Einen interpolierten String anzuhängen (sb.Append($"...")) ist in Ordnung und liest sich gut. Ab .NET 6 schreibt der Compiler die Teile sogar direkt in den Builder, ohne den Zwischenstring zu erzeugen.
AppendLine verwendet Environment.NewLine: \n unter Linux und macOS, \r\n unter Windows. Wenn ein Dateiformat ein bestimmtes Zeilenende verlangt, hänge es selbst an.
Insert, Remove, Replace und der Indexer
Anders als ein String lässt sich ein StringBuilder direkt bearbeiten:
Ausgabe:
Hello, World
Hello, Maya
Maya
maya
maya!
[] length 0
Length auf einen kleineren Wert zu setzen kürzt den Inhalt; das ist der günstigste Weg, ein überzähliges Trennzeichen am Ende zu entfernen. Clear() setzt die Länge auf 0 und behält den reservierten Puffer, ein Builder lässt sich also über die Durchläufe einer äußeren Schleife wiederverwenden.
Der Indexer ist für kürzlich angehängte Zeichen schnell, kann aber bei wahlfreiem Zugriff in einen sehr großen Builder langsam sein, weil der Inhalt in verketteten Blöcken gespeichert ist. Wenn du Zeichen lesen musst, rufe einmal ToString() auf und indiziere den String.
Das Problem mit dem Trennzeichen am Ende
Ein klassischer Einsatz ist eine kommagetrennte Liste. Wer nach jedem Element ein Trennzeichen anhängt, hat am Ende eines zu viel:
Ausgabe:
csharp, dotnet, linq
csharp, dotnet, linq
Wenn die Elemente schon in einer Collection liegen, ist string.Join kürzer und mindestens so schnell. Behalte StringBuilder für Text, dessen Form von Logik abhängt: bedingte Abschnitte, verschachtelte Schleifen, gemischte Formatierung.
Kapazität
Ein StringBuilder startet mit Platz für 16 Zeichen. Wenn der Platz ausgeht, hängt er einen neuen Speicherblock an, der so groß ist wie alles, was er schon enthält, bis zu 8.000 Zeichen pro Block. Seine Kapazität verdoppelt sich also, solange er klein ist, und wächst danach in Schritten von 8.000 Zeichen. Beim Wachsen wird der bereits gespeicherte Text nie kopiert. Wenn du ungefähr weißt, wie lang das Ergebnis wird, übergib die Kapazität an den Konstruktor, dann muss der Builder nie wachsen:
Ausgabe:
12000
10000
True
True
Die Größe vorab festzulegen ist eine Optimierung, keine Pflicht. Das Wachsen allokiert einen neuen Block, verschiebt aber nie den vorhandenen Text, der Standard ist also in Ordnung, außer du baust viele große Strings in einem heißen Pfad.
Wann du StringBuilder nicht brauchst
StringBuilder ist kein schnellerer Ersatz für jedes +. Der Compiler macht aus a + b + c + d in einem einzigen Ausdruck bereits einen Aufruf von string.Concat, der die Endlänge berechnet und jeden Teil einmal kopiert: dieselbe Arbeit, die ein Builder mit mehr Code erledigen würde. Nimm einfache Strings, wenn:
- Die Anzahl der Teile klein und fest ist:
$"{first} {last} ({age})". - Die Teile in einer Collection liegen und ein Trennzeichen brauchen:
string.Join(", ", items). - Die Teile kein Trennzeichen brauchen:
string.Concat(parts). - Du ein paar Mal außerhalb jeder Schleife verkettest.
Nimm StringBuilder, wenn du in einer Schleife anhängst, im Voraus nicht weißt, wie oft du anhängst, oder den Text beim Aufbauen bearbeitest (einfügen, ersetzen, entfernen). Hin und her umzuwandeln (sb.ToString() in der Schleife) verschenkt den Vorteil; rufe es einmal am Ende auf.
Streams sind die andere Alternative: Um eine große Datei zu erzeugen, schreibe Zeilen nach und nach in einen StreamWriter, statt den ganzen Inhalt im Speicher aufzubauen. Siehe Dateien.
Häufig gestellte Fragen
Was ist StringBuilder in C#?
System.Text.StringBuilder ist ein veränderlicher Zeichenpuffer, um einen String in vielen Schritten aufzubauen. Append hängt an das Ende des Puffers an, statt jedes Mal einen neuen String zu erzeugen, und ToString() erzeugt den fertigen String einmal. Er ist das Standardwerkzeug, um Text in einer Schleife aufzubauen.
Wann sollte ich StringBuilder statt String-Verkettung verwenden?
Wenn du in einer Schleife oder unbekannt oft anhängst, etwa beim Erstellen eines Berichts, einer CSV-Datei oder von HTML aus einer Collection. Für eine feste Handvoll Teile (a + b + c oder ein interpolierter String) ist einfache Verkettung genauso schnell und leichter zu lesen, weil der Compiler sie bereits zu einem einzigen Aufruf von string.Concat zusammenfasst.
Wie entferne ich das letzte Zeichen aus einem StringBuilder?
Verringere seine Length: if (sb.Length > 0) sb.Length--; entfernt das letzte Zeichen ohne Kopie. sb.Remove(sb.Length - 1, 1) tut dasselbe. So entfernt man üblicherweise ein überzähliges Komma am Ende, wobei string.Join das überzählige Trennzeichen gar nicht erst entstehen lässt.
Ist StringBuilder threadsicher?
Nein. Rufen mehrere Threads gleichzeitig Append auf demselben StringBuilder auf, kann sein Inhalt beschädigt werden oder eine Exception fliegen. Gib jedem Thread einen eigenen Builder und kombiniere die Ergebnisse, oder schütze den gemeinsamen mit einem lock.
Was fügt AppendLine in C# hinzu?
Den Text gefolgt von Environment.NewLine, das unter Windows \r\n und unter Linux und macOS \n ist. Wenn die Ausgabe ein bestimmtes Zeilenende braucht, etwa \n für ein Dateiformat oder \r\n für einen HTTP-Header, hänge es stattdessen explizit mit Append("\n") an.