StringBuilder and Regular Expressions

Two tools for heavier text work. StringBuilder builds long strings efficiently, avoiding the cost of immutable strings. Regular expressions describe text patterns — "a date", "an email address", "three digits" — so you can validate, search, extract and replace text that simple string methods can't handle.

Why StringBuilder?

Because strings are immutable, s += "x" in a loop creates a brand-new string on every pass and copies all the old text into it. For a few pieces that's fine; for thousands it becomes slow. A StringBuilder keeps a growable buffer and only creates the final string once:

C#
using System.Text;

var sb = new StringBuilder();
for (int i = 1; i <= 5; i++)
{
    sb.Append(i);
    if (i < 5)
    {
        sb.Append(", ");
    }
}
string result = sb.ToString();
Console.WriteLine(result);
Console.WriteLine(sb.Length);
Output
1, 2, 3, 4, 5
13

Useful StringBuilder methods

Most methods return the builder itself, so calls can be chained:

C#
using System.Text;

var report = new StringBuilder();
report.AppendLine("REPORT")
      .AppendLine(new string('-', 6))
      .Append("Items: ").Append(3).AppendLine()
      .AppendLine($"Total: {29.97m:C}");

report.Replace("REPORT", "SALES REPORT");
report.Insert(0, ">> ");
Console.Write(report);
Output
>> SALES REPORT
------
Items: 3
Total: $29.97
MethodDoes
Append(x)Adds text (or any value's ToString()) to the end
AppendLine(x)Adds text and a line break
Insert(index, x)Inserts text at a position
Replace(old, new)Replaces in place
Remove(start, length)Deletes characters
Clear()Empties the builder for reuse
ToString()Produces the finished string

Rule of thumb: use interpolation or + for a handful of pieces, string.Join for a collection, and StringBuilder when appending inside a loop.

Regular expressions

A regular expression ("regex") is a mini-language for patterns. The Regex class lives in System.Text.RegularExpressions. Use verbatim strings (@"...") for patterns so backslashes don't need doubling:

C#
using System.Text.RegularExpressions;

string[] codes = ["AB-1234", "ab-1234", "AB-12345", "XY-0042"];

foreach (string code in codes)
{
    bool valid = Regex.IsMatch(code, @"^[A-Z]{2}-\d{4}$");
    Console.WriteLine($"{code,-9} {valid}");
}
Output
AB-1234   True
ab-1234   False
AB-12345  False
XY-0042   True

Pattern building blocks

PatternMatches
\d \w \sA digit; a word character (letter, digit, _); whitespace
.Any character except a line break
[abc] [a-z] [^0-9]One of a set; a range; anything not in the set
* + ?Zero or more; one or more; zero or one of the previous item
{3} {2,4}Exactly 3; between 2 and 4
^ $Start and end of the text
\bA word boundary
(…) (?<name>…)A capture group; a named group
a|bEither alternative

Without ^ and $, IsMatch succeeds if the pattern appears anywhere in the text — a common validation mistake.

Extracting with groups

Regex.Match returns details about the first match, including the text captured by each group:

C#
using System.Text.RegularExpressions;

string log = "2026-10-08 14:32:07 ERROR Disk almost full (97%)";
Match m = Regex.Match(log, @"^(?<date>\d{4}-\d{2}-\d{2}) (?<time>[\d:]+) (?<level>[A-Z]+) (?<msg>.*)$");

if (m.Success)
{
    Console.WriteLine($"Date:    {m.Groups["date"].Value}");
    Console.WriteLine($"Time:    {m.Groups["time"].Value}");
    Console.WriteLine($"Level:   {m.Groups["level"].Value}");
    Console.WriteLine($"Message: {m.Groups["msg"].Value}");
}
Output
Date:    2026-10-08
Time:    14:32:07
Level:   ERROR
Message: Disk almost full (97%)

Finding every match

C#
using System.Text.RegularExpressions;

string text = "Call 555-0142 or 555-0199. Fax: 555-0100.";

foreach (Match m in Regex.Matches(text, @"\b\d{3}-\d{4}\b"))
{
    Console.WriteLine($"{m.Value} at position {m.Index}");
}

string hashtags = "Loving #dotnet and #csharp! #100DaysOfCode";
var tags = Regex.Matches(hashtags, @"#(\w+)").Select(m => m.Groups[1].Value);
Console.WriteLine(string.Join(", ", tags));
Output
555-0142 at position 5
555-0199 at position 17
555-0100 at position 32
dotnet, csharp, 100DaysOfCode

Replacing and splitting

Regex.Replace can refer to groups in the replacement with $1 or ${name}:

C#
using System.Text.RegularExpressions;

Console.WriteLine(Regex.Replace("too    many     spaces", @"\s+", " "));
Console.WriteLine(Regex.Replace("2026-10-08", @"(\d{4})-(\d{2})-(\d{2})", "$3/$2/$1"));
Console.WriteLine(Regex.Replace("card 4111 1111 1111 1234", @"\d{4} (?=\d{4})", "**** "));
Console.WriteLine(string.Join("|", Regex.Split("a1b22c333d", @"\d+")));
Output
too many spaces
08/10/2026
card **** **** **** 1234
a|b|c|d

The (?=…) in the third pattern is a lookahead: it requires four more digits to follow without including them in the match, so the last group is left visible.

Options and generated regexes

Pass RegexOptions for case-insensitive matching and more. If you use a pattern often, the [GeneratedRegex] attribute makes the compiler generate optimised matching code at build time:

C#
using System.Text.RegularExpressions;

Console.WriteLine(Regex.IsMatch("HELLO world", "hello", RegexOptions.IgnoreCase));

foreach (string s in new[] { "user@example.com", "not-an-email", "a.b@c.org" })
{
    Console.WriteLine($"{s}: {Patterns.Email().IsMatch(s)}");
}

static partial class Patterns
{
    [GeneratedRegex(@"^[^@\s]+@[^@\s]+\.[a-z]{2,}$", RegexOptions.IgnoreCase)]
    public static partial Regex Email();
}
Output
True
user@example.com: True
not-an-email: False
a.b@c.org: True

A word of caution: regexes are powerful but quickly become unreadable. For simple checks, string methods are clearer. And for truly complex formats such as full email validation or HTML, use a dedicated parser rather than one giant pattern.

Try it yourself

Pull every price out of a sentence and add them up. Then change the pattern to also accept prices without cents.

Try it yourself · Program.cs
Edit the code, copy it into Program.cs and run it with dotnet run Program.cs (.NET 10 or later). How to set up .NET
Output
Found 12.50
Found 3.25
Found 6.75
Total: $22.50