Strings, Interpolation & StringBuilder

Reviewed & published by Brayan K

Text is everywhere — names, messages, file contents, reports. This lesson goes well beyond basic printing: you'll master string interpolation and formatting, the essential string methods for cleaning and searching text, verbatim strings for paths and multi-line text, and StringBuilder for efficiently building large strings.

Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

An immutable string is like a printed photograph. You can't edit the photo itself — if you want a brighter version, you make a new print; the original stays exactly as it was. That's why name.ToUpper() hands you a new string and leaves name alone. A StringBuilder, by contrast, is the editable digital file on your computer: you tweak it as many times as you like in place, and only "print" (call ToString()) once you're done.

1. Interpolation & Formatting

Interpolation — the $"..." syntax — is the modern way to build text from values. After a colon you can add a format specifier (:C, :F2, :P0) and after a comma an alignment width for neat columns. It's far more readable than gluing strings together with +.

using System.Globalization;
using System;

class Program
{
    static void Main()
    {
        // Currency formatting follows the thread's culture: {value:C} prints
        // £ in London, $ in Boston and € in Paris. Pin it when the output has
        // to be the same everywhere — as it does on a page that shows you the
        // result.
        CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-GB");

        string first = "Ada";
        string last  = "Lovelace";
        int    born  = 1815;

        // Interpolation: $"..." drops values straight into the text.
        Console.WriteLine($"{first} {last} was born in {born}.");
        // Ada Lovelace was born in 1815.

        // Format specifiers go after a colon inside the braces:
        decimal pay = 1234.5m;
        Console.WriteLine($"Pay: {pay:C}");      // Pay: £1,234.50
        Console.WriteLine($"Pi:  {Math.PI:F3}"); // Pi:  3.142

        // Alignment: {value,width} pads to a column width (negative = left).
        Console.WriteLine($"|{first,-10}|{born,6}|");
        // |Ada       |  1815|

        // The older String.Format does the same with numbered placeholders:
        Console.WriteLine(string.Format("{0} {1}", first, last));
    }
}

// ✅ Expected output:
//    Ada Lovelace was born in 1815.
//    Pay: £1,234.50
//    Pi:  3.142
//    |Ada       |  1815|
//    Ada Lovelace

2. The Essential String Methods

Strings come with a rich toolkit. Trim removes surrounding whitespace, ToUpper/ToLower change case, Replace swaps text, Contains/IndexOf search, and Substring extracts a slice. Split turns text into an array and string.Join glues an array back into text. Remember: every one of these returns a new string — the original never changes.

using System;

class Program
{
    static void Main()
    {
        string raw = "  Hello, World!  ";

        // Strings are IMMUTABLE — each method returns a NEW string.
        Console.WriteLine($"[{raw.Trim()}]");          // [Hello, World!]
        Console.WriteLine(raw.Trim().ToUpper());       // HELLO, WORLD!
        Console.WriteLine(raw.Trim().ToLower());       // hello, world!
        Console.WriteLine(raw.Length);                 // 17 (includes spaces)

        string trimmed = raw.Trim();
        Console.WriteLine(trimmed.Replace("World", "C#")); // Hello, C#!
        Console.WriteLine(trimmed.Contains("World"));      // True
        Console.WriteLine(trimmed.IndexOf("World"));       // 7
        Console.WriteLine(trimmed.Substring(0, 5));        // Hello
        Console.WriteLine(trimmed.StartsWith("Hello"));    // True

        // Split text into parts; Join glues them back together.
        string csv = "red,green,blue";
        string[] parts = csv.Split(',');
        Console.WriteLine(parts.Length);               // 3
        Console.WriteLine(string.Join(" | ", parts));  // red | green | blue
    }
}

// ✅ Expected output:
//    [Hello, World!]
//    HELLO, WORLD!
//    hello, world!
//    17
//    Hello, C#!
//    True
//    7
//    Hello
//    True
//    3
//    red | green | blue

Your turn. Clean up a messy name and reformat it to proper case. The trickier capitalisation lines are written for you — fill in the Trim blank.

using System;

class Program
{
    static void Main()
    {
        // 🎯 YOUR TURN — clean up and reformat a messy full name.
        string messy = "   brenda   sellers   ";

        // 1) Trim the surrounding spaces  (hint: .Trim())
        string clean = ___;            // 👉 messy.Trim()

        // 2) Split on spaces, removing the empty gaps between words
        string[] words = clean.Split(' ', StringSplitOptions.RemoveEmptyEntries);

        // 3) Build "Brenda Sellers" — capitalise each word's first letter.
        string first = char.ToUpper(words[0][0]) + words[0].Substring(1);
        string last  = char.ToUpper(words[1][0]) + words[1].Substring(1);

        Console.WriteLine($"{first} {last}");
        // ✅ Expected output:  Brenda Sellers
    }
}

3. StringBuilder for Heavy Lifting

Because strings are immutable, building one with += inside a loop secretly creates a fresh string every iteration — quadratic work that crawls for large inputs. StringBuilder keeps one growable buffer and mutates it with Append and AppendLine, materialising the final string only when you call ToString(). Reach for it whenever you build text in a loop.

using System;
using System.Text;

class Program
{
    static void Main()
    {
        // ❌ The slow way: each += makes a brand-new string (O(n^2) in a loop).
        // string report = "";
        // for (int i = 1; i <= 5; i++) report += $"Line {i}\n";

        // ✅ StringBuilder mutates ONE internal buffer — efficient in loops.
        var sb = new StringBuilder();
        for (int i = 1; i <= 5; i++)
        {
            sb.AppendLine($"Line {i}");
        }
        sb.Append("--- end ---");

        // Nothing is materialised until you call ToString().
        string report = sb.ToString();
        Console.WriteLine(report);
        // Line 1
        // Line 2
        // Line 3
        // Line 4
        // Line 5
        // --- end ---
    }
}

// ✅ Expected output:
//    Line 1
//    Line 2
//    Line 3
//    Line 4
//    Line 5
//    --- end ---

🔎 Deep Dive: verbatim & raw strings

A verbatim string @"..." treats backslashes literally — perfect for Windows paths and regex patterns — and lets the text span multiple lines exactly as typed. Prefix with both $@ (or @$) to get interpolation and verbatim behaviour together.

using System;

class Program
{
    static void Main()
    {
        // @"..." is a VERBATIM string: backslashes are literal, no escaping.
        string path = @"C:\Reports\2026\summary.txt";
        Console.WriteLine(path); // C:\Reports\2026\summary.txt

        // @ also lets a string span multiple lines exactly as written:
        string sql = @"SELECT id, name
FROM users
WHERE active = 1";
        Console.WriteLine(sql);

        // Combine @ and $ for a multi-line interpolated string:
        string user = "Ada";
        string note = $@"Dear {user},
  Your report is ready.
Thanks!";
        Console.WriteLine(note);
    }
}

// ✅ Expected output:
//    C:\Reports\2026\summary.txt
//    SELECT id, name
//    FROM users
//    WHERE active = 1
//    Dear Ada,
//      Your report is ready.
//    Thanks!

Newer C# (11+) also offers raw string literals with triple quotes """...""", which handle embedded quotes and indentation cleanly — great for JSON or HTML blocks. Verbatim strings remain the workhorse you'll see most often.

🧩 Reorder Challenge

These six lines build a comma-separated list with a StringBuilder and print red, green, blue. Put them in working order.

Console.WriteLine(sb.ToString());
var sb = new StringBuilder();
foreach (string c in colours)
    sb.Append(c + ", ");
string[] colours = { "red", "green", "blue" };
string result = sb.ToString().TrimEnd(',', ' ');
// (these six lines go inside Main; print 'result' at the end)
string[] colours = { "red", "green", "blue" };
var sb = new StringBuilder();
foreach (string c in colours)
    sb.Append(c + ", ");
string result = sb.ToString().TrimEnd(',', ' ');
Console.WriteLine(result);

Why: colours must exist before the loop iterates it, and the StringBuilder must exist before Append is called. The loop fills the buffer, then we trim the trailing ", " into result, and print it last. (The stray sb.ToString() line was a distractor — the real final string is result.)

🧠 Quick Recall

1. What does this print?

string s = "Hello";
s.ToUpper();
Console.WriteLine(s);

Hello — strings are immutable, so ToUpper() returns a new string that's thrown away. You'd need s = s.ToUpper(); to keep it.

2. What is the output?

string[] p = "a-b-c".Split('-');
Console.WriteLine(p.Length);

3 — Split('-') breaks the text into ["a", "b", "c"], an array of length 3.

3. What prints here?

Console.WriteLine($"{42,5}");

" 42" — the ,5 right-aligns 42 in a 5-character field, padding with three spaces.

Pro Tips

Common Errors (and the fix)

Mini-Challenge: Build an Invoice

Use a StringBuilder to assemble a multi-line invoice, formatting each price as currency and summing a total. The outline is in the comments.

using System;
using System.Text;

class Program
{
    static void Main()
    {
        // 🎯 MINI-CHALLENGE: Build a formatted invoice line by line.
        // 1. Create a StringBuilder.
        // 2. Append a header line: "INVOICE".
        // 3. For each of these items, append "<name> .... <price:C>":
        //      ("Keyboard", 29.99m), ("Mouse", 14.50m), ("Cable", 4.00m)
        // 4. Append a TOTAL line summing the prices (use :C).
        // 5. Print sb.ToString().
        //
        // ✅ Example output:
        //    INVOICE
        //    Keyboard .... £29.99
        //    Mouse .... £14.50
        //    Cable .... £4.00
        //    TOTAL: £48.49

        // your code here
    }
}

🎉 Lesson Complete

Practice quiz

Why are C# strings immutable?

  • They can be changed in place
  • They are always empty
  • Once created, their characters never change; methods return new strings
  • They store numbers only

Answer: Once created, their characters never change; methods return new strings. Immutable means a string's contents never change; methods like ToUpper return a brand-new string.

What does s.ToUpper() do if you don't assign its result back to s?

  • Returns a new string that is discarded, leaving s unchanged
  • Changes s in place
  • Throws an error
  • Empties s

Answer: Returns a new string that is discarded, leaving s unchanged. Strings are immutable, so ToUpper returns a new string; without s = s.ToUpper(); the original is unchanged.

What is the length of the array from "a-b-c".Split('-') ?

  • 1
  • 2
  • 4
  • 3

Answer: 3. Split('-') breaks the text into ["a", "b", "c"], an array of length 3.

Which class is best for building a large string in a loop?

  • String
  • StringBuilder
  • List
  • Array

Answer: StringBuilder. StringBuilder mutates one internal buffer (O(n)), avoiding the quadratic cost of += in a loop.

What does string.Join(" | ", parts) do?

  • Glues array elements together with a separator
  • Splits text into parts
  • Trims whitespace
  • Reverses the string

Answer: Glues array elements together with a separator. Join combines the elements of an array into one string, separated by the given text.

What does a verbatim string @"C:\Reports" treat backslashes as?

  • Escape characters
  • New lines
  • Literal characters
  • Errors

Answer: Literal characters. In a @"..." verbatim string, backslashes are literal — no escaping needed, ideal for file paths.

Which method removes whitespace from both ends of a string?

  • Replace
  • Trim
  • Substring
  • Split

Answer: Trim. Trim() returns a new string with surrounding whitespace removed.

What does "Yes" == "yes" evaluate to?

  • true
  • an error
  • null
  • false

Answer: false. String comparison with == is case-sensitive by default, so "Yes" and "yes" are not equal.

What does the alignment in $"{42,5}" produce?

  • "42 "
  • " 42"
  • "42"
  • "00042"

Answer: " 42". A positive width right-aligns the value in a field of that width, padding the left with spaces: " 42".

When is StringBuilder unnecessary?

  • When looping thousands of times
  • Never use plain strings
  • For just a couple of joins
  • Only with numbers

Answer: For just a couple of joins. For a handful of concatenations, plain + or interpolation is fine and more readable; reach for StringBuilder in loops.

Continue this course

Frequently asked questions

Why are C# strings immutable, and what does that mean?

Once a string is created its characters can never change. Methods like ToUpper, Replace and Trim do not edit the original — they return a brand-new string and leave the old one untouched. Immutability makes strings safe to share between threads and easy to reason about, but it means heavy looping concatenation creates lots of throwaway strings, which is where StringBuilder comes in.

When should I use StringBuilder instead of the + operator?

Use a StringBuilder when you build a string in a loop or across many steps — say more than a handful of concatenations. Because each + creates a new string, concatenating in a long loop is O(n squared); StringBuilder mutates an internal buffer and is O(n). For a couple of joins, plain + or interpolation is perfectly fine and more readable.

What is the difference between string interpolation and String.Format?

They do the same job. Interpolation with $"..." places the values directly inside the braces and is easier to read. String.Format uses numbered placeholders like {0} and {1} with the arguments listed afterwards. Interpolation is preferred in modern C#; the compiler actually turns it into similar code under the hood.

How do I compare strings ignoring case?

Use string.Equals(a, b, StringComparison.OrdinalIgnoreCase) rather than converting both to lower case first. It is clearer, avoids allocating temporary strings, and uses the comparison rules you intend. For sorting or culture-aware comparison there are other StringComparison options, but OrdinalIgnoreCase is the right default for things like usernames.

Related lessons