StringBuilder

One growable buffer instead of a new string per append: what StringBuilder fixes, and when a plain string is still the better answer.

SyntaxStringBuilder builder = new(); builder.Append(value); string result = builder.ToString();

Strings are immutable, so text += piece; cannot extend the string in place. It allocates a new string large enough for both parts, copies the old contents into it, then copies the new piece. Do that in a loop and each round copies everything collected so far. A thousand appends perform roughly half a million character copies and throw away a thousand strings.

StringBuilder, in the System.Text namespace, is a mutable buffer of characters. Append writes into free space in that buffer and only grows it when it runs out, which happens rarely because each growth gives room for many more appends. Nothing is copied per append in the normal case. ToString() produces the finished string once, at the end.

Using it

Append has overloads for string, char, int, bool and more, so numbers need no conversion first. Each returns the same builder, which is why calls chain. AppendLine appends text and a line break. Insert, Replace and Remove edit what is already in the buffer. Length is the character count so far, and Clear() empties the buffer so one builder can be reused.

When not to use it

For a known, small number of pieces, interpolation or string.Join is clearer and already allocates once. A builder for three fragments is slower, not faster, because the builder itself is an allocation. Use it when the number of appends is a loop or is unknown, and when it matters, measure rather than guess.

Example

Example · csharp
using System.Text;

// A loop runs the same lines repeatedly; here i is 1, then 2, up to 5.
// Each += builds a whole new string and copies everything collected so far.
string slow = "";
for (int i = 1; i <= 5; i++)
{
    slow += i + ",";
}

// A StringBuilder holds one growable buffer and writes into it.
StringBuilder builder = new StringBuilder();
for (int i = 1; i <= 5; i++)
{
    // Append returns the same builder, so the calls chain.
    builder.Append(i).Append(',');
}

// ToString produces the finished string once, at the end.
string fast = builder.ToString();

Console.WriteLine(slow);
Console.WriteLine(fast);
Console.WriteLine(slow == fast);   // same text, far less copying
Console.WriteLine(builder.Length); // characters currently in the buffer

// Output:
// 1,2,3,4,5,
// 1,2,3,4,5,
// True
// 10

When to use it

  • Assemble a CSV or fixed-width export line by line from a database query.
  • Build a long log or diagnostic message from an unknown number of fragments.
  • Produce a plain-text email body from a list of order items.
  • Replace a += loop that profiling has shown to dominate a request's allocations.

More examples

When Join is the better answer

Reaching for StringBuilder out of habit is a common over-correction. When the items are already in an array or list, string.Join is shorter, allocates once and needs no ToString call. AppendJoin exists for the case where you want that joining behaviour as part of a larger build.

Example · csharp
using System.Text;

// A collection expression (C# 12). Same as: new string[] { "a", "b", "c" }
string[] parts = ["a", "b", "c"];

// The pieces already exist, so Join does the whole job in one allocation.
string joined = string.Join(", ", parts);

// A builder earns its place when you are also writing text around them.
// new() is shorthand for new StringBuilder(); the type comes from the left side.
StringBuilder builder = new();
builder.Append('[');
builder.AppendJoin(", ", parts); // same separator logic, straight into the buffer
builder.Append(']');

Console.WriteLine(joined);
Console.WriteLine(builder.ToString());

// Output:
// a, b, c
// [a, b, c]

Deterministic line breaks

Anything whose exact bytes matter - a CSV export, an HTTP body, a file another system parses, or a value compared in a test - should not depend on which operating system built it. Note that Append(3) took the int directly: the overload formats it for you, with no ToString call and no extra string.

Example · csharp
using System.Text;

StringBuilder report = new();

// AppendLine uses Environment.NewLine, which differs between operating systems.
// Appending "\n" yourself makes the output identical everywhere.
report.Append("name,qty").Append('\n');
report.Append("widget").Append(',').Append(3).Append('\n');
report.Append("bolt").Append(',').Append(120).Append('\n');

string csv = report.ToString();

Console.Write(csv);
Console.WriteLine(csv.Split('\n', StringSplitOptions.RemoveEmptyEntries).Length);

// Output:
// name,qty
// widget,3
// bolt,120
// 3

Pre-sizing and reusing a builder

Capacity is how many characters fit before the buffer has to grow; a rough estimate removes the remaining copies in a hot loop. Clear is what you want between items when one builder serves a whole batch. Forgetting it is a classic bug: the second value comes out with the first one still glued to the front of it.

Example · csharp
using System.Text;

// Giving the expected capacity up front avoids growing the buffer later.
StringBuilder builder = new StringBuilder(capacity: 64);

for (int i = 0; i < 3; i++)
{
    builder.Append("row ").Append(i).Append(';');
}

Console.WriteLine(builder.ToString());
Console.WriteLine(builder.Length);

// Clear empties the buffer so the same builder can produce the next value.
builder.Clear();
builder.Append("second use");

Console.WriteLine(builder.ToString());
Console.WriteLine(builder.Length);

// Output:
// row 0;row 1;row 2;
// 18
// second use
// 10

Discussion

  • Be the first to comment on this lesson.