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:
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);
1, 2, 3, 4, 5 13
Useful StringBuilder methods
Most methods return the builder itself, so calls can be chained:
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);
>> SALES REPORT ------ Items: 3 Total: $29.97
| Method | Does |
|---|---|
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:
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}"); }
AB-1234 True ab-1234 False AB-12345 False XY-0042 True
Pattern building blocks
| Pattern | Matches |
|---|---|
\d \w \s | A 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 |
\b | A word boundary |
(…) (?<name>…) | A capture group; a named group |
a|b | Either 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:
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}"); }
Date: 2026-10-08 Time: 14:32:07 Level: ERROR Message: Disk almost full (97%)
Finding every match
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));
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}:
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+")));
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:
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(); }
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.
Found 12.50 Found 3.25 Found 6.75 Total: $22.50