Skip to main content
C# beginner Lesson 8 of 25

Strings in C#

String interpolation, verbatim strings, Span<char>, StringBuilder, and regex in C#.

String Basics

Strings in C# are immutable sequences of Unicode characters. The string keyword is an alias for System.String. Immutability means that every operation that appears to “modify” a string — like ToUpper or Replace — actually returns a new string, leaving the original untouched. This is safe and simple, but it means you need tools like StringBuilder when building strings incrementally.

string name = "Alice";
string empty = "";
string? nullable = null;

// Length
Console.WriteLine(name.Length);        // 5

// Character access (0-indexed)
Console.WriteLine(name[0]);            // A
Console.WriteLine(name[^1]);           // e (index from end, C# 8+)

// Substring — returns a new string (original unchanged)
Console.WriteLine(name[1..3]);           // li  (range syntax, C# 8+)
Console.WriteLine(name.Substring(1, 3)); // lic (classic API)

// Searching
Console.WriteLine(name.Contains("lic"));      // True
Console.WriteLine(name.StartsWith("Al"));     // True
Console.WriteLine(name.EndsWith("ce"));       // True

// Case conversion — always returns a new string
Console.WriteLine(name.ToUpper());    // ALICE
Console.WriteLine(name.ToLower());    // alice

// Trim whitespace
string padded = "  hello  ";
Console.WriteLine(padded.Trim());         // "hello"
Console.WriteLine(padded.TrimStart());    // "hello  "
Console.WriteLine(padded.TrimEnd());      // "  hello"

String Interpolation

String interpolation ($"") is the recommended way to embed values and expressions in strings. It is more readable than string.Format and safer than manual concatenation because the compiler validates the expressions inside the braces at compile time.

string firstName = "Alice";
int age = 30;
double score = 98.6;

// Basic interpolation — embed any expression in { }
string msg = $"Hello, {firstName}! You are {age} years old.";

// Expressions are evaluated inline
Console.WriteLine($"Double age: {age * 2}");
Console.WriteLine($"Is adult: {age >= 18}");

// Format specifiers — control how the value is displayed
Console.WriteLine($"Score: {score:F1}");            // 98.6  (1 decimal place)
Console.WriteLine($"Score: {score:P0}");             // 9,860% (percentage)
Console.WriteLine($"Now: {DateTime.Now:yyyy-MM-dd}"); // ISO date
Console.WriteLine($"Price: {1234.5m:C}");            // $1,234.50 (currency)

// Column alignment — negative = left-align, positive = right-align
Console.WriteLine($"{"Name",-10} {"Score",8}");
foreach (var (n, s) in scores)
    Console.WriteLine($"{n,-10} {s,8:F1}");

// Multi-line with $@ — combine interpolation with verbatim for JSON/SQL templates
string json = $@"{{
  ""name"": ""{firstName}"",
  ""age"": {age}
}}";

Verbatim Strings

Verbatim strings (@"") treat backslashes as literal characters. This makes them essential for file paths and multi-line content where the normal escape sequences would make the string nearly unreadable.

// Without verbatim — every backslash must be doubled
string path = "C:\\Users\\Alice\\Documents\\file.txt";

// With verbatim — backslashes are literal, much easier to read
string path2 = @"C:\Users\Alice\Documents\file.txt";

// Multi-line verbatim — preserves line breaks and indentation
string sql = @"
    SELECT *
    FROM Orders
    WHERE Status = 'Active'
    ORDER BY CreatedAt DESC";

// To include a double-quote inside a verbatim string, double it up
string quote = @"She said ""hello"" to me.";

// Combine $ and @ for interpolated verbatim strings (useful for SQL with parameters)
string query = $@"
    SELECT *
    FROM {tableName}
    WHERE UserId = {userId}";

Raw String Literals (C# 11+)

Raw string literals eliminate the need for any escaping at all. They are ideal for embedding JSON, XML, or regex patterns where escaping would obscure the actual content. The number of leading quotes (three or more) determines the delimiter, so you can always choose a delimiter that does not appear in the content itself.

// Triple-quote raw string — no escaping needed for quotes or backslashes
string json = """
    {
        "name": "Alice",
        "age": 30
    }
    """;

// Interpolated raw string — expressions still work
string name = "Alice";
string xml = $"""
    <person name="{name}" />
    """;

String Methods

The System.String class provides a rich set of methods for common operations. Knowing the right method avoids reinventing common patterns and produces cleaner, more expressive code.

string text = "  Hello, World!  ";

// Split — divide a string into parts by a delimiter
string[] words = "one,two,three".Split(',');  // ["one", "two", "three"]
string[] lines = text.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);

// Join — combine a collection of strings with a separator
string joined = string.Join(", ", new[] { "Alice", "Bob", "Carol" });  // "Alice, Bob, Carol"
string csv = string.Join(",", numbers.Select(n => n.ToString()));

// Replace — substitute all occurrences
string replaced = "foo bar foo".Replace("foo", "baz");  // "baz bar baz"

// IndexOf — find the position of a substring
int idx = "Hello World".IndexOf("World");   // 6

// Compare — use StringComparison to avoid locale surprises
int cmp = string.Compare("apple", "banana", StringComparison.Ordinal);
bool eq  = string.Equals("ABC", "abc", StringComparison.OrdinalIgnoreCase);

// Null and empty checks — prefer these over == "" to handle null gracefully
string.IsNullOrEmpty(text);        // true if null or ""
string.IsNullOrWhiteSpace(text);   // true if null, "", or only whitespace

StringBuilder

StringBuilder exists because strings are immutable. Each + in a loop creates a new string object and copies all previous content into it, resulting in O(N²) allocations. StringBuilder maintains a mutable internal buffer and only creates a final string when you call ToString(), giving you O(N) performance.

using System.Text;

// Bad — O(N²): creates 10,000 temporary string objects
string result = "";
for (int i = 0; i < 10_000; i++)
    result += i + ",";

// Good — O(N): StringBuilder grows a buffer, converts to string once at the end
var sb = new StringBuilder(capacity: 80_000);  // pre-allocate if you know the size
for (int i = 0; i < 10_000; i++)
{
    sb.Append(i);
    sb.Append(',');
}
string result2 = sb.ToString();

// StringBuilder API
var builder = new StringBuilder();
builder.Append("Hello");
builder.AppendLine(", World!");        // appends text + newline
builder.AppendFormat("Score: {0:F1}", 98.6);
builder.Insert(0, ">>> ");             // insert at a specific index
builder.Replace("Hello", "Hi");       // in-place replacement
builder.Remove(0, 4);                 // remove 4 characters at index 0
Console.WriteLine(builder.Length);
Console.WriteLine(builder.ToString());

Span<char> for Zero-Allocation Parsing

Span<char> gives you a read-only view into an existing string’s memory. Slicing a Span does not allocate — it just adjusts the start pointer and length. This is invaluable in high-throughput parsing code where creating thousands of substring objects would put real pressure on the garbage collector.

string csv = "Alice,30,London";
ReadOnlySpan<char> span = csv.AsSpan();  // no allocation — a view into csv's memory

// Split without allocating substrings
int firstComma = span.IndexOf(',');
ReadOnlySpan<char> nameSpan = span[..firstComma];    // "Alice" — no allocation
ReadOnlySpan<char> rest     = span[(firstComma + 1)..]; // "30,London"

int secondComma = rest.IndexOf(',');
ReadOnlySpan<char> ageSpan  = rest[..secondComma];      // "30"
ReadOnlySpan<char> citySpan = rest[(secondComma + 1)..]; // "London"

// Parse int directly from span — still no string allocation
int age = int.Parse(ageSpan);

Console.WriteLine($"Name: {nameSpan}, Age: {age}, City: {citySpan}");
// Name: Alice, Age: 30, City: London

Regular Expressions

Regular expressions provide a concise pattern language for searching, validating, and transforming strings. They are most useful when the structure you are matching is too complex for simple string methods — multi-part patterns, optional segments, or repeated groups.

using System.Text.RegularExpressions;

string text = "Order #12345 placed on 2024-01-15 for $99.99";

// Simple match — does the string contain a 5-digit number?
bool hasOrderNumber = Regex.IsMatch(text, @"\d{5}");  // true

// Extract the first match
var dateMatch = Regex.Match(text, @"\d{4}-\d{2}-\d{2}");
if (dateMatch.Success)
    Console.WriteLine($"Date: {dateMatch.Value}");  // 2024-01-15

// Named capture groups — reference by name instead of index
var pattern = @"Order #(?<order>\d+).*?(?<amount>\$[\d.]+)";
var match = Regex.Match(text, pattern);
Console.WriteLine(match.Groups["order"].Value);   // 12345
Console.WriteLine(match.Groups["amount"].Value);  // $99.99

// Find all matches
string emails = "Contact alice@example.com or bob@test.org for support";
var emailMatches = Regex.Matches(emails, @"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}");
foreach (Match m in emailMatches)
    Console.WriteLine(m.Value);

// Replace — clean up extra whitespace
string cleaned = Regex.Replace("  Hello   World  ", @"\s+", " ").Trim();
Console.WriteLine(cleaned);  // "Hello World"

// Compiled regex — for patterns used repeatedly in hot paths
private static readonly Regex PhoneRegex =
    new(@"^\+?1?\s?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$",
        RegexOptions.Compiled);

// Source-generated regex (C# 11+) — best performance, zero runtime compilation cost
public partial class Validator
{
    [GeneratedRegex(@"^\w+@\w+\.\w+$")]
    private static partial Regex EmailPattern();

    public bool IsValidEmail(string s) => EmailPattern().IsMatch(s);
}

String Performance Tips

Understanding how strings behave under the hood lets you avoid common performance traps and choose the right tool for each scenario.

// 1. Use string.Concat or interpolation for a handful of items — not repeated +
string s = string.Concat(firstName, " ", lastName);

// 2. Use StringComparison for case-insensitive comparison — avoids locale issues
bool eq = s.Equals("alice smith", StringComparison.OrdinalIgnoreCase);

// 3. Use string.Create for custom formatting without intermediate strings
string formatted = string.Create(10, 42, static (span, value) =>
{
    value.TryFormat(span, out _);
});

// 4. stackalloc + Span for small char buffers — stays on the stack, no GC pressure
Span<char> buffer = stackalloc char[64];
int written;
DateTime.Now.TryFormat(buffer, out written, "yyyy-MM-dd");
ReadOnlySpan<char> dateStr = buffer[..written];

Frequently Asked Questions

Why is string concatenation in a loop bad?
Each + on a string creates a new string object because strings are immutable. A loop that concatenates N times creates N intermediate strings, giving O(N²) time and memory. Use StringBuilder for loops.
What is the difference between string and String?
They are the same type. 'string' is a C# keyword alias for System.String. Use 'string' for variables and 'String' only when calling static methods (String.Format, String.IsNullOrEmpty) — though the lowercase versions work fine too.
When should I use Span<char> over string?
Use Span<char> in performance-critical code where you need to work with a substring without allocating a new string. Span<char> is a stack-only view into existing memory, so it is zero-allocation.