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?
- Safe sharing. Passing a string to a method can never let the method change "your" copy — there's nothing to change. (Classes are the opposite: reference aliasing.)
- Free interning. Identical literals can safely share one storage — which is why
"apple" == "apple"can be a reference comparison that still says True. - Hashability. Immutable keys can't have their hash drift — that's why strings work as
Dictionarykeys.
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?
| Situation | Use | Why |
|---|---|---|
Two or three pieces, once (name + " is " + age) | + / interpolation | Readable; one allocation anyway |
| Building text in a loop | StringBuilder | O(n) instead of O(n²) |
| Joining a collection | string.Join | Purpose-built, done in one call |
| Immutable text value | string | It'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?