Strings in C# are immutable: s += "x" does not extend s, it creates a new string containing the old characters plus the new ones, and the old string becomes garbage. Once is nothing. In a loop that runs thousands of times, each iteration copies everything built so far, so the total work grows with the square of the length.
StringBuilder, in System.Text, is a mutable buffer. Append writes into spare room at the end, growing the buffer occasionally, and ToString() makes one string at the end.
Output:
Order #1042, total 59.97
Status: shipped
**********
51
Why concatenation in a loop is slow
Here is the same text built both ways. Timings vary by machine, so the numbers below are only an example; the gap is what matters, and it widens as the loop grows:
Example output:
string += 482 ms, length 188890
StringBuilder 1 ms, length 188890
With 20,000 appends, the += version copies almost 2 billion characters in total. StringBuilder copies each character about twice, once when it is appended and once more in ToString(), which is why it stays near a millisecond.
Append, AppendLine and AppendFormat
Append has overloads for every built-in type, and it returns the same builder, so calls can be chained. AppendLine adds a newline after its argument. AppendFormat takes a composite format string like string.Format:
Output:
RECEIPT
-------
Espresso 2.40
Croissant 3.10
Orange juice 4.50
TOTAL 10.00
Appending an interpolated string (sb.Append($"...")) is fine and reads well. On .NET 6 and later the compiler even writes the pieces straight into the builder without creating the intermediate string.
AppendLine uses Environment.NewLine: \n on Linux and macOS, \r\n on Windows. When a file format requires a particular line ending, append it yourself.
Insert, Remove, Replace and the indexer
Unlike a string, a StringBuilder can be edited in place:
Output:
Hello, World
Hello, Maya
Maya
maya
maya!
[] length 0
Setting Length to a smaller value truncates; it is the cheapest way to remove a trailing separator. Clear() sets the length to 0 and keeps the allocated buffer, so one builder can be reused across iterations of an outer loop.
The indexer is fast for recent appends but can be slow for random access into a very large builder, because the content is stored in linked chunks. If you need to read characters, call ToString() once and index the string.
The trailing separator problem
A classic use is building a comma-separated list. Appending a separator after every item leaves one too many at the end:
Output:
csharp, dotnet, linq
csharp, dotnet, linq
When the items are already in a collection, string.Join is shorter and at least as fast. Keep StringBuilder for text whose shape depends on logic: conditional sections, nested loops, mixed formatting.
Capacity
A StringBuilder starts with room for 16 characters. When it runs out, it links on a new block of storage as large as everything it already holds, up to 8,000 characters per block, so its capacity doubles while it is small and then grows in 8,000-character steps. Growing never copies the text already stored. If you know roughly how long the result will be, pass the capacity to the constructor and the builder never has to grow:
Output:
12000
10000
True
True
Presizing is an optimization, not a requirement. Growth allocates a new block but never moves the existing text, so the default is fine unless you are building many large strings in a hot path.
When not to use StringBuilder
StringBuilder is not a faster replacement for every +. The compiler already turns a + b + c + d in a single expression into one string.Concat call, which computes the final length and copies each piece once, the same work a builder would do with more code. Use plain strings when:
- The number of pieces is small and fixed:
$"{first} {last} ({age})". - The pieces are in a collection and need a separator:
string.Join(", ", items). - The pieces need no separator:
string.Concat(parts). - You concatenate a few times outside any loop.
Use StringBuilder when you append inside a loop, append a number of times you do not know in advance, or edit the text (insert, replace, remove) while building it. Converting back and forth (sb.ToString() inside the loop) throws the benefit away; call it once at the end.
Streams are the other alternative: to produce a large file, write lines to a StreamWriter as you go instead of building the whole content in memory. See files.
Frequently Asked Questions
What is StringBuilder in C#?
System.Text.StringBuilder is a mutable buffer of characters for building a string in many steps. Append adds to the end of the buffer instead of creating a new string each time, and ToString() produces the final string once. It is the standard tool for building text in a loop.
When should I use StringBuilder instead of string concatenation?
When you append in a loop or an unknown number of times, such as building a report, a CSV file or HTML from a collection. For a fixed handful of pieces (a + b + c, or one interpolated string) plain concatenation is just as fast and easier to read, because the compiler already combines it into a single string.Concat call.
How do I remove the last character from a StringBuilder?
Reduce its Length: if (sb.Length > 0) sb.Length--; drops the last character without copying. sb.Remove(sb.Length - 1, 1) does the same. This is the usual way to strip a trailing comma, though string.Join avoids the trailing separator in the first place.
Is StringBuilder thread-safe?
No. Calling Append on the same StringBuilder from several threads at once can corrupt its contents or throw. Give each thread its own builder and combine the results, or protect the shared one with a lock.
What does AppendLine add in C#?
The text followed by Environment.NewLine, which is \r\n on Windows and \n on Linux and macOS. If the output must use a specific line ending, such as \n for a file format or \r\n for an HTTP header, append it explicitly with Append("\n") instead.