Types · strings

string vs StringBuilder in C#

The headline fact that explains everything: C# strings are immutable — no string method ever changes a string; they all return a new one. StringBuilder exists because of that fact.

Immutability, demonstrated

Run this and watch a method call do… nothing:

string s = "hello";
s.Replace("l", "L");        // returns a NEW string — s is untouched
Console.WriteLine(s);
string s = "hello";

s = s.Replace("l", "L");    // capture the new string back
Console.WriteLine(s);       // heLLo

s = s.ToUpper();            // same rule for every string method
Console.WriteLine(s);       // HELLO

// the ORIGINAL literal never changed — new strings were built instead:
Console.WriteLine("original still intact: " + ("hello" == "hello"));

Once it clicks, a whole bug family disappears: "why doesn't my Replace do anything?" — because you dropped the return value. Strings are values that happen to be text; treat every method's result as the actual output.

Why immutability, anyway?

The cost: building strings in a loop

string msg = "";
for (int i = 1; i <= 5; i++)
{
    msg += i + ",";     // each += builds a brand-new string: old + new part
}
Console.WriteLine(msg);

Five iterations, fine. Now the math: iteration n copies all n−1 accumulated characters to make the new string. Total copying grows as 1+2+3+…+n ≈ n²/2 characters. At n = 100,000 that's billions of character copies and a wall of garbage for the collector. That's the bug StringBuilder exists to fix.

StringBuilder: a mutable buffer

StringBuilder keeps an internal buffer and appends into it — no new string per append, no copying of previous content. You only pay once, at the end, when you ask for the finished string:

// StringBuilder is not in the browser engine's subset — runs as-is in Visual Studio
using System.Text;

var sb = new StringBuilder();
for (int i = 1; i <= 100000; i++)
{
    sb.Append(i);
    sb.Append(',');
}
string msg = sb.ToString();     // one string at the very end

Try the loop version with n = 100,000 in the engine here and feel the difference; then trust that StringBuilder does the same job in a blink. (The engine's List<T> + string.Join is the in-engine equivalent: var parts = new List<string>(); … string.Join(",", parts).)

So which one, when?

SituationUseWhy
Two or three pieces, once (name + " is " + age)+ / interpolationReadable; one allocation anyway
Building text in a loopStringBuilderO(n) instead of O(n²)
Joining a collectionstring.JoinPurpose-built, done in one call
Immutable text valuestringIt's the default — and the safety net

Rule of thumb: few pieces → interpolate; loop → StringBuilder; collection → Join. Don't pre-optimize the two-piece case — $"{name} is {age}" is both the fastest to write and plenty fast to run.

One-minute self-test

Both lines print — predict each before running in the playground:

string s = "cat";
s.Replace("c", "b");
Console.WriteLine(s);        // "bat" or "cat"? (immutability)

s = s.Replace("c", "b");
Console.WriteLine(s);        // and now?