“In our daily lives as programmers, we process text strings a lot. So I tried to work hard on text processing, namely the string class and regular expressions.”
— Yukihiro Matsumoto, creator of Ruby
Summary
String Creation & Basics — Four literal syntaxes: standard "...", verbatim @"..." (no escape processing), raw """...""" (C# 11+, indentation-trimmed), and interpolated $"...". string is an immutable UTF-16 reference type; == compares content, not reference. Convert.ToString is null-safe; ?.ToString() ?? fallback is the idiomatic null-safe conversion. string.IsNullOrEmpty tests null and "", IsNullOrWhiteSpace also catches whitespace-only values.
Indexing & Slicing — 0-based [] indexing; ^n indexes from the end (C# 8+); .. Range syntax returns a substring (right-exclusive). Substring(start, length) is the classic API. No built-in stride — use LINQ Where with index. Out-of-range access throws IndexOutOfRangeException or ArgumentOutOfRangeException immediately; no silent failure.
String Methods — ToUpper/ToLower/ToTitleCase for case; Trim/TrimStart/TrimEnd accept custom chars; PadLeft/PadRight for fixed-width output. char.IsXxx classifies individual characters; LINQ.All(char.IsXxx) tests whole strings. IndexOf/LastIndexOf/Contains/StartsWith/EndsWith all accept StringComparison. Replace has no max-count; Split accepts StringSplitOptions.RemoveEmptyEntries and .TrimEntries (.NET 5+). Encoding.UTF8.GetBytes/GetString convert to/from byte arrays.
String Formatting — $"" interpolation is preferred and compiles to DefaultInterpolatedStringHandler (.NET 6+). string.Format with {index,alignment:format} is used when the format string is a runtime variable. Standard format specifiers: F/N/E/G/P/C/D/X. Culture-aware formatting via ToString("C2", new CultureInfo("fr-FR")).
Efficient String Building — StringBuilder uses a mutable char[] buffer; O(n) vs O(n²) for += in loops. Key methods: Append, AppendLine, Insert, Replace, Remove. Pre-allocate with new StringBuilder(estimatedCapacity). string.Join is the single-allocation path for collections; string.Concat for separator-free assembly. ReadOnlySpan<char> via string.AsSpan() enables zero-allocation slicing; string.Create writes directly into a new string’s buffer via SpanAction<char, TState>.
Regular Expressions — Regex.Match (first match), Regex.Matches (all matches), Regex.IsMatch (boolean). Numbered groups via Groups[n]; named groups via (?<name>...) and Groups["name"]. Regex.Replace accepts a MatchEvaluator lambda or $n backreferences. RegexOptions.Compiled pre-emits IL; [GeneratedRegex] (.NET 7+) does this at build time, is AOT-compatible, and is the preferred path. RegexOptions.IgnorePatternWhitespace enables inline # comments. NonBacktracking option guarantees linear time.
Glossary
string
Immutable sequence of UTF-16 code units represented by System.String, with value-based equality for content comparison.
Used for textual data such as names, JSON, SQL fragments, file paths, messages, and protocol values.
Interning is a niche optimization
string.Intern can reduce duplication in some repeated-string workloads, but it keeps interned strings alive for a long time and is not a general-purpose performance fix.
char
Single UTF-16 code unit represented by System.Char.
Used for low-level character inspection, tokenization, parsing, and APIs that operate one code unit at a time.
char.IsXxx methods are Unicode-aware
Methods such as char.IsLetter, char.IsDigit, and char.IsWhiteSpace understand Unicode categories, not just ASCII.
Immutability
Property of an object whose observable value cannot change after creation.
Used to explain why string operations always return a new value instead of modifying the original instance in place.
Repeated concatenation can become quadratic
Building a long string via repeated += in a loop can cause many intermediate allocations and repeated copying. Use StringBuilder or string.Join when the workload is append-heavy.
String interpolation
C# syntax for embedding expressions directly inside a string literal using $"...".
Used to create readable formatted strings without manual placeholder indexing or repeated concatenation.
Do not interpolate untrusted data into SQL or shell commands
Interpolation is formatting, not sanitization. Use parameterized queries and command APIs designed for structured arguments rather than constructing executable text directly.
Verbatim string
String literal written as @"...", where backslashes are treated literally and line breaks can be embedded directly.
Used to reduce escaping noise in Windows paths, regex patterns, and other strings containing many backslashes.
Interpolation and verbatim syntax can be combined
$@"..." and @$"..." are both valid, allowing interpolation and verbatim behavior in the same literal.
Raw string literal
C# 11+ string-literal form written with triple quotes such as """...""", designed to minimize escaping and preserve text more naturally.
Used for embedded JSON, SQL, regex, XML, and other multi-line text where ordinary escaping would hurt readability.
Raw literals can also be interpolated
Prefixing with $ enables interpolation, and additional $ characters raise the brace-count threshold needed for interpolation markers.
StringBuilder
Mutable text-construction type in System.Text optimized for repeated append, insert, and replace operations without allocating a new string for every step.
Used when a string must be built incrementally through many mutations, especially inside loops or streaming text-generation code.
string.Join is often better for joining collections
When you already have a collection of strings, string.Join is often simpler and very efficient. Reach for StringBuilder when the text is being assembled procedurally.
Span<char>
Stack-only ref struct representing a mutable view over a contiguous region of char data.
Used for allocation-free slicing and in-place character work inside performance-sensitive parsing and formatting code.
Span<T> is stack-only, not necessarily stack-backed memory
A Span<char> can point to stack memory, array memory, or string-related memory exposed safely by APIs. The restriction is that the span value itself cannot be stored on the heap or cross await boundaries.
ReadOnlySpan<char>
Read-only stack-only view over contiguous char data.
Used to slice and parse text without allocating substrings, especially when calling modern APIs that accept span overloads.
Check that the target framework actually has the overload
Many parsing and search APIs accept ReadOnlySpan<char>, but not all versions of .NET expose the same overload set. Confirm availability before assuming zero-allocation behavior.
StringComparison
Enum that controls whether string operations use ordinal or culture-aware rules, and whether comparison is case-sensitive.
Used to make comparison intent explicit in APIs such as Equals, Compare, StartsWith, EndsWith, IndexOf, and related methods.
Prefer ordinal comparisons for technical identifiers
Ordinal or OrdinalIgnoreCase is usually the right choice for keys, protocol tokens, config values, and other non-linguistic text. Culture-aware comparisons are mainly for human-language scenarios.
String interning
Runtime mechanism that allows identical strings to share one canonical instance in an intern pool.
Used to reduce duplication in specialized scenarios with very high repetition of identical string values.
Interning trades memory duplication for lifetime retention
Interned strings can remain alive for a long time, so interning indiscriminately can increase retained memory rather than reduce it. Use it only for clearly repetitive, bounded vocabularies.
Regex
System.Text.RegularExpressions.Regex type for pattern matching, extraction, replacement, and splitting using regular expressions.
Used when text logic needs expressive pattern matching that goes beyond simple literal search.
Recreating regex objects in hot loops is wasteful
Parsing a pattern repeatedly adds overhead. Cache reusable regex instances, use static helpers when appropriate, or adopt [GeneratedRegex] for compile-time-known patterns.
[GeneratedRegex]
.NET source-generation attribute that produces a strongly typed regex factory method at compile time for a known pattern.
Used to avoid repeated runtime regex parsing and to improve performance and deployment characteristics for compile-time-known expressions.
Use it for stable, compile-time-known patterns
[GeneratedRegex] is most appropriate when the pattern is fixed in source code. It requires a partial method declaration and is generally preferred over ad hoc new Regex(...) for those cases on modern .NET.
Capture group
Parenthesized part of a regex pattern that records the text matched by that subexpression, either by numeric index or by explicit name.
Used to extract structured pieces of a larger match, such as IDs, dates, tokens, or named components.
Use non-capturing groups when extraction is not needed
(?:...) groups for precedence and quantifiers without populating the capture collection, which keeps the match model simpler.
string.Format()
Composite-formatting API that fills numbered placeholders such as {0} and {1:N2} using supplied arguments.
Used when the format string is determined at runtime, stored in resources, or otherwise not convenient to express as an interpolated literal.
Interpolation is usually clearer in new code
Prefer $"..." when the format is local and static in source. Keep string.Format for dynamic format templates, reusable resources, or APIs that already work with composite formatting.
This note covers C#‘s string type in full: creation and immutability, indexing and range syntax, the complete set of string methods, interpolation and composite formatting, StringBuilder for efficient construction, Span<char> for zero-allocation parsing, and regular expressions including .NET 7’s source-generated [GeneratedRegex].
String Creation & Basics
This section covers the fundamental building blocks of string handling in C#: literal syntax, type conversions, construction patterns, and the immutability guarantee that shapes how strings behave at runtime.
Literals and declaration
C# provides several literal syntaxes for creating strings, from standard double-quoted literals to verbatim and raw string literals that simplify paths and multiline content.
Declare a string and a char
string (alias for System.String) represents an immutable sequence of UTF-16 characters. char represents a single Unicode character occupying 2 bytes. Double quotes delimit strings; single quotes delimit char values.
This example shows how to declare a string and a char.
Prefix a string literal with @ to create a verbatim string where backslashes are treated as literal characters. This eliminates double-backslash escaping for file paths, regex patterns, and any text containing backslashes. Verbatim and escaped strings produce identical runtime values.
This example shows how to disable escape processing with verbatim strings.
Create multiline strings with verbatim and raw literals
Verbatim strings (@"") preserve line breaks exactly as written. Raw string literals (C# 11+) use triple quotes (""") and automatically trim leading whitespace based on the indentation of the closing delimiter, making them ideal for embedded SQL, JSON, or XML.
This example prints the same multiline content from a verbatim string and a raw string literal.
string s4 = @"This isa multilinestring";Console.WriteLine(s4);string s5 = """ This is a raw string literal """;Console.WriteLine(s5);
This isa multilinestringThis is araw string literal
Conversion and construction
Methods for converting other types to strings, assembling strings from parts, and checking for empty or null values.
Convert other types to string with ToString and Convert
Every type in C# inherits ToString() from System.Object, making it the universal conversion path. Convert.ToString() adds null safety — it returns string.Empty instead of throwing on null. For nullable references, chain ?.ToString() with ?? to supply a fallback.
This example shows how to convert other types to string with ToString and Convert.
C# has no * operator for string repetition. Use new string(char, count) for single-character repeats, or string.Concat with Enumerable.Repeat for multi-character patterns. The + operator concatenates strings and is optimized by the compiler for small, fixed concatenations.
This example shows how to repeat and concatenate strings.
C# strings have three distinct “nothing” states: null (no object), "" (empty string with zero length), and whitespace-only (contains only spaces, tabs, or newlines). string.IsNullOrEmpty catches the first two; string.IsNullOrWhiteSpace catches all three. Prefer string.Empty over "" for clarity when declaring empty strings.
IsNullOrEmpty vs IsNullOrWhiteSpace
Use IsNullOrWhiteSpace when validating user input or external data — whitespace-only strings are almost never meaningful. Reserve IsNullOrEmpty for internal logic where whitespace may be intentional (e.g., formatting strings).
This example shows how to check for empty, null, and whitespace strings.
The single most important property of System.String — every operation that appears to modify a string actually allocates a new object and returns it. Understanding this shapes how you write performant string code.
Understand string immutability and its implications
Once a string is created, its character sequence cannot change. Indexing into a string with assignment (s[0] = 'H') is a compile error. Any transformation — ToUpper(), Replace(), Substring(), or concatenation — allocates a new string on the heap. The original is unchanged and becomes eligible for garbage collection if no other reference points to it.
Every apparent mutation creates a brand-new string, so loop-based += grows
both copying cost and allocation pressure with each iteration.
Concatenation in loops turns string building into O(n^2) work
Each += in a loop creates a new string object and copies all previous
characters. For n iterations this becomes O(n^2) in both time and
allocations.
Use the construction strategy that matches the workload
Use StringBuilder for loop-based construction, string.Join for
collections, and + or $"" only for small fixed concatenations.
This example shows that string methods return new values instead of modifying the original string instance.
string s = "hello";s = 'H' + s.Substring(1);Console.WriteLine(s);
Hello
The diagram below shows what happens in memory. The variable s is reassigned to point to a new string object; the original "hello" is not modified — it becomes unreachable and is collected by the GC.
flowchart LR
subgraph Before["Before: s = #quot;hello#quot;"]
BPAD[" "]
s1["s"] -->|points to| obj1["#quot;hello#quot;<br/>(heap)"]
BPAD ~~~ s1
end
subgraph After["After: s = 'H' + s.Substring(1)"]
APAD[" "]
s2["s"] -->|points to| obj2["#quot;Hello#quot;<br/>(new object)"]
obj3["#quot;hello#quot;<br/>(unreachable → GC)"]
APAD ~~~ s2
APAD ~~~ obj3
end
Before --> After
style BPAD fill:transparent,stroke:transparent,color:transparent
style APAD fill:transparent,stroke:transparent,color:transparent
Indexing & Slicing
Accessing individual characters and extracting substrings. C# uses 0-based indexing, hat (^) indexing from the end, and Range syntax (..) for slicing — all without allocating intermediate collections.
Accessing characters and substrings
Direct character access by position, substring extraction, and Range syntax for slicing.
Access characters and substrings by index and range
C# strings support 0-based indexing with [], hat indexing from the end with ^, and Range syntax with .. for slicing. All return values without modifying the original string.
Indexing and slicing
s[i] — returns a char at position i
s[^i] — indexes from the end (^1 = last char), eliminating s[s.Length - i]
s[a..b] — substring via Range syntax (C# 8+), right-exclusive (s[0..5] = indices 0-4)
No step/stride support — use LINQ for every-nth-char
For pattern extraction, use Regex or Split instead of index math
This example shows how to access characters and substrings by index and range.
string s = "Hello, World!";Console.WriteLine($"'{s[0]}'");Console.WriteLine($"'{s[1]}'");Console.WriteLine($"'{s[^1]}'");Console.WriteLine($"'{s[^2]}'");Console.WriteLine($"'{s[0..5]}'");Console.WriteLine($"'{s[..5]}'");Console.WriteLine($"'{s[7..]}'");Console.WriteLine($"'{s[^6..]}'");Console.WriteLine($"'{s[7..12]}'");
'H''e''!''d''Hello''Hello''World!''World!''World'
Extract substrings and stride through characters
Substring(startIndex) and Substring(startIndex, length) are the classic APIs; Range syntax (s[7..]) is the modern equivalent. C# has no built-in step/stride parameter — use LINQ Where with an index predicate for every-nth-character patterns, or Reverse() to reverse a string.
This example shows how to extract substrings and stride through characters.
Accessing a character beyond the string length throws IndexOutOfRangeException. Using a Range that exceeds bounds throws ArgumentOutOfRangeException. Neither returns null or a default — C# fails fast on invalid access.
String indexing is fail-fast, not forgiving
Dynamic positions have to be validated explicitly because C# throws as soon as
an index or range exceeds the string bounds.
Out-of-range access does not fail silently
Unlike languages that may return an empty string or None, C# throws
immediately on invalid access. Always validate indices against s.Length
when positions come from data.
Guard access before indexing
Check bounds before indexing, or use a guarded access pattern such as:
string safe = index < s.Length ? s[index].ToString() : "(out of range)";
s[100] → IndexOutOfRangeExceptions[0..100] → ArgumentOutOfRangeException (range must be within bounds)
Iterating characters
Three patterns for walking through a string character by character: foreach for simple iteration, LINQ Select for index-value pairs, and the classic for loop for index-based access.
Iterate with foreach
foreach yields each char in sequence. Combined with Range syntax, you can iterate over a substring without allocating a copy in older runtimes (C# 8+ Range on string returns a new string, but the iteration itself is straightforward).
This example shows how to iterate with foreach.
bool firstChar = true;foreach (char ch in s[..5]){ if (!firstChar) Console.Write(" "); Console.Write(ch); firstChar = false;}Console.WriteLine();
H e l l o
Enumerate characters with LINQ Select
LINQ’s Select overload provides both the character and its index as a tuple, similar to Python’s enumerate(). Destructure the tuple in the foreach header for clean access.
This example pairs each character with its index by using LINQ Select inside a foreach loop.
The classic index-based for loop gives direct control over start, end, and step. Prefer this when you need to skip characters, iterate in reverse, or access adjacent indices within the loop body.
This example shows how to iterate with a for loop.
for (int i = 0; i < 5; i++) Console.WriteLine($" [{i}] = '{s[i]}'");
[0] = 'H' [1] = 'e' [2] = 'l' [3] = 'l' [4] = 'o'
String Methods
The built-in methods on System.String for transforming case, trimming whitespace, inspecting content, searching, splitting, joining, and encoding. All transformation methods return new strings — the original is never modified.
Case conversion
Methods for changing letter case. C# provides ToUpper(), ToLower(), and ToTitleCase() (via TextInfo). There are no built-in swapcase or casefold equivalents.
Convert case with ToUpper, ToLower, and ToTitleCase
ToUpper() and ToLower() convert all characters in a string. ToTitleCase() is available via CultureInfo.CurrentCulture.TextInfo and capitalizes the first letter of each word. All three return new strings — the original is unchanged.
Case methods
ToUpper() / ToLower() — convert all characters
ToTitleCase() — via CultureInfo.CurrentCulture.TextInfo, capitalizes each word
No built-in swapcase or casefold
Culture-aware: ToUpper(CultureInfo) handles locale-specific rules (e.g., Turkish i → İ)
Case-insensitive comparison should not be implemented by normalizing both sides manually
Converting strings to uppercase or lowercase first creates unnecessary
allocations and can still produce locale-sensitive bugs.
ToUpper() or ToLower() is the wrong comparison primitive
Do not use ToUpper() for case-insensitive comparison. It allocates a new
string and can behave incorrectly for locale edge cases such as Turkish I.
Use StringComparison.OrdinalIgnoreCase directly
Pass StringComparison.OrdinalIgnoreCase to string.Equals, IndexOf,
StartsWith, or Contains so no intermediate uppercase string is created:
Methods for stripping leading/trailing whitespace (or custom characters) and padding strings to a fixed width.
Trim, TrimStart, TrimEnd, and Pad
Trim() removes whitespace from both ends; TrimStart()/TrimEnd() remove from one side only. Pass a char argument to trim specific characters instead of whitespace. PadLeft/PadRight extend a string to a target width, filling with spaces or a specified character — useful for tabular output and zero-padding.
This example demonstrates trim, TrimStart, TrimEnd, and Pad.
Inspecting individual characters and testing whole strings for content categories.
Check character categories with char.IsXxx
The char struct provides static methods to classify individual characters: IsLetter, IsDigit, IsWhiteSpace, IsUpper, IsLower, and others. These are Unicode-aware — IsLetter returns true for letters in any script, not just ASCII.
This example applies the main char.IsXxx classification helpers to individual characters.
C# has no built-in str.isalpha() or str.isdigit() like Python. Instead, combine LINQ’s All method with char.IsXxx predicates to test whether every character in a string satisfies a condition.
This example tests whole strings by combining LINQ All with char.IsXxx predicates.
Console.WriteLine("Hello".All(char.IsLetter));Console.WriteLine("12345".All(char.IsDigit));Console.WriteLine("Hello123".All(char.IsLetterOrDigit));Console.WriteLine("HELLO".All(char.IsUpper));Console.WriteLine("hello".All(char.IsLower));Console.WriteLine("Hello".All(c => c < 128));
TrueTrueTrueTrueTrueTrue
Search and location
Methods for finding substrings by position or existence.
Search with IndexOf, Contains, StartsWith, EndsWith
IndexOf returns the 0-based position of the first occurrence (or -1 if not found). Pass a startIndex to search from a specific position. LastIndexOf searches from the end. Contains, StartsWith, and EndsWith return bool for existence checks. All support a StringComparison overload for case-insensitive matching. C# has no built-in occurrence counter — split on the target and subtract one, or use Regex.Matches(...).Count.
This example shows how to search with IndexOf, Contains, StartsWith, EndsWith.
Methods for comparing strings for equality and sort order — distinct from searching by position.
Compare strings for ordering with string.Compare and CompareTo
string.Compare returns a negative, zero, or positive int indicating sort order. CompareTo is the instance-method equivalent but defaults to CurrentCulture comparison — making it culture-sensitive and inappropriate for programmatic keys. string.CompareOrdinal performs a fast code-point-by-code-point comparison without culture rules — use it for identifiers, keys, and paths where linguistic sorting is irrelevant.
Ordinal vs culture-aware comparison
Use StringComparison.Ordinal (or OrdinalIgnoreCase) for internal identifiers, dictionary keys, file paths, and protocol strings. Use StringComparison.CurrentCulture only when displaying sorted results to users where locale-specific ordering matters (e.g., German ä sorting near a).
This example shows how to compare strings for ordering with string.Compare and CompareTo.
Methods for substituting substrings, tokenizing strings into arrays, and reassembling arrays into strings.
Replace substrings and split strings into arrays
Replace substitutes all occurrences — there is no max-count parameter (use Regex.Replace with a count for first-only replacement). Split tokenizes a string by a delimiter and returns a string[]. Pass an int count to limit the number of resulting parts. Splitting on null with RemoveEmptyEntries splits on any whitespace and discards empty segments, equivalent to Python’s str.split().
This example shows how to replace substrings and split strings into arrays.
Control split behavior with StringSplitOptions and rejoin with Join
StringSplitOptions.RemoveEmptyEntries discards empty segments from consecutive delimiters. TrimEntries (.NET 5+) trims whitespace from each segment after splitting. string.Join reassembles an array with a separator; string.Concat joins without any separator.
This example shows how to control split behavior with StringSplitOptions and rejoin with Join.
Console.WriteLine($"[{string.Join(", ", "a,,b,,c".Split(',', StringSplitOptions.RemoveEmptyEntries))}]");Console.WriteLine($"[{string.Join(", ", " a , b , c ".Split(',', StringSplitOptions.TrimEntries))}]");string[] parts = { "hello", "world", "csharp" };Console.WriteLine($"'{string.Join(' ', parts)}'");Console.WriteLine($"'{string.Join(", ", parts)}'");Console.WriteLine($"'{string.Join("->", parts)}'");Console.WriteLine($"'{string.Concat(parts)}'");
[a, b, c][a, b, c]'hello world csharp''hello, world, csharp''hello->world->csharp''helloworldcsharp'
Encoding
Converting between strings (UTF-16 in memory) and byte arrays for I/O, hashing, or network transmission.
Convert between strings and byte arrays with Encoding
Encoding.UTF8.GetBytes serializes a string to a UTF-8 byte array; GetString reverses the process. Use Encoding.ASCII for 7-bit ASCII or Encoding.Unicode for UTF-16LE. For pure ASCII strings, UTF-8 and ASCII produce identical bytes.
BOM awareness
Encoding.UTF8 does not emit a BOM (byte order mark). Use new UTF8Encoding(true) if a BOM is required for file interoperability.
This example shows how to convert between strings and byte arrays with Encoding.
Embedding values into strings and controlling how numbers, dates, and currencies are displayed. C# offers three mechanisms: string interpolation ($""), String.Format, and ToString with format specifiers.
Interpolation and composite formatting
The primary ways to embed expressions into string literals.
Embed values with string interpolation
String interpolation ($"") is the preferred approach. Expressions inside {...} are evaluated at runtime and converted to strings. You can place any valid C# expression inside the braces — arithmetic, method calls, ternary operators. For alignment and format specifiers, use the syntax {expr,alignment:format}.
This example shows how to embed values with string interpolation.
string name = "Alice";int age = 30;double n = 1234567.89123;double pct = 0.856;Console.WriteLine($"Name: {name}, Age: {age}");Console.WriteLine(age + 1);Console.WriteLine(name.ToUpper());
Name: Alice, Age: 3031ALICE
Format strings with String.Format (composite formatting)
String.Format is the older API that uses numbered placeholders ({0}, {1}). It is still useful when the format string is stored externally (resource files, config) or constructed dynamically — interpolation requires a compile-time literal. The placeholder syntax supports the same alignment and format specifiers as interpolation: {index,alignment:format}.
This example formats values with String.Format and composite-format placeholders.
Standard format strings passed to ToString("X") or inside interpolation braces {value:X} control how numbers are displayed.
Format numbers with standard specifiers
Specifier
Meaning
Example
F
Fixed-point
1234567.89
N
Number with group separator
1,234,567.89
E
Scientific notation
1.23E+06
G
General (compact)
1235
P
Percentage (multiplies by 100)
85.6%
C
Currency (locale-aware)
$1,234,567.89
D
Decimal (integers only)
255
X/x
Hexadecimal upper/lower
FF / ff
Append a digit to control precision: F2 = 2 decimal places, D8 = zero-padded to 8 digits. Use Convert.ToString(value, base) for binary (base 2) or octal (base 8).
This example applies standard numeric format specifiers for fixed-point, grouping, scientific, percent, currency, and radix output.
Console.WriteLine(n.ToString("F2"));Console.WriteLine(n.ToString("F0"));Console.WriteLine(n.ToString("N2"));Console.WriteLine(n.ToString("E2"));Console.WriteLine(n.ToString("G4"));Console.WriteLine(pct.ToString("P1"));Console.WriteLine(n.ToString("C2"));int x = 255;Console.WriteLine(x.ToString("D"));Console.WriteLine(x.ToString("X"));Console.WriteLine(x.ToString("x"));Console.WriteLine(x.ToString("D8"));Console.WriteLine(Convert.ToString(x, 2));Console.WriteLine(Convert.ToString(x, 8));
Controlling field width for tabular output and formatting numbers according to locale conventions.
Align strings and format currencies by culture
PadLeft/PadRight align strings within a fixed-width field. For culture-specific formatting, pass a CultureInfo instance to ToString — the runtime uses the culture’s number group separator, decimal separator, and currency symbol. This is essential for financial applications that must display amounts in the user’s locale.
This example aligns strings with padding and formats the same currency amount under several cultures.
string s = "hi";Console.WriteLine($"'{s.PadLeft(10, '*')}'");Console.WriteLine($"'{s.PadRight(10, '*')}'");double amt = 1234567.89;Console.WriteLine(amt.ToString("C2", new CultureInfo("en-US")));Console.WriteLine(amt.ToString("C2", new CultureInfo("fr-FR")));Console.WriteLine(amt.ToString("C0", new CultureInfo("ja-JP")));Console.WriteLine(amt.ToString("C2", new CultureInfo("zh-CN")));Console.WriteLine(amt.ToString("C2", new CultureInfo("pt-BR")));Console.WriteLine(amt.ToString("C2", new CultureInfo("en-GB")));
Strategies for building strings without the O(n²) penalty of repeated concatenation. StringBuilder for loops, string.Join for collections, and + for small fixed concatenations.
flowchart TD
A["How many strings<br/>are you combining?"] --> B{"2–5 fixed parts?"}
B -->|Yes| C["Use + or $#quot;#quot;<br/>Compiler optimizes this"]
B -->|No| D{"From a collection<br/>or array?"}
D -->|Yes| E["Use string.Join<br/>Single allocation"]
D -->|No| F{"Built in a loop<br/>or dynamically?"}
F -->|Yes| G["Use StringBuilder<br/>O(n) vs O(n²)"]
F -->|No| H["Use string.Concat<br/>No separator needed"]
StringBuilder
StringBuilder maintains a mutable internal char[] buffer, appending in place without allocating new string objects on every operation.
Compare StringBuilder vs string + performance
StringBuilder
Modifies an internal char buffer in place — Append/AppendLine/Insert/Replace
O(n) for n appends vs O(n²) for string + in a loop
Pre-allocate capacity for known sizes: new StringBuilder(1024)
Use for building strings in loops, large template assembly, CSV generation
For simple concatenation (2–5 strings), + or $"" is cleaner; for collections, string.Join
The benchmark below compares repeated += concatenation against StringBuilder.Append and reports a deterministic faster/slower summary instead of machine-specific timings.
This example compares repeated string += concatenation with StringBuilder.Append and reports a deterministic faster/slower result.
var sw = Stopwatch.StartNew();string result = "";for (int i = 0; i < 50000; i++) result += i.ToString();sw.Stop();var t1 = sw.Elapsed.TotalSeconds;sw.Restart();var sb = new StringBuilder();for (int i = 0; i < 50000; i++) sb.Append(i);result = sb.ToString();sw.Stop();var t2 = sw.Elapsed.TotalSeconds;Console.WriteLine($"+ in loop len={result.Length}");Console.WriteLine($"StringBuilder len={result.Length}");Console.WriteLine($"StringBuilder faster? {t2 < t1}");
+ in loop len=238890StringBuilder len=238890StringBuilder faster? True
Build strings with Append, Insert, Replace, Remove
Append adds to the end, AppendLine adds a string plus a newline, Insert places text at a specific index, Replace swaps substrings in place, and Remove deletes a range. Pre-allocate capacity with the constructor to avoid internal buffer resizing when the final size is known.
This example modifies a StringBuilder with Append, AppendLine, Insert, Replace, and capacity inspection.
sb = new StringBuilder("Hello");sb.Append(", ");sb.Append("World!");sb.AppendLine();sb.AppendLine($"{42}");sb.Insert(0, ">>> ");sb.Replace("World", "C#");Console.WriteLine(sb);Console.WriteLine(sb.Length);Console.WriteLine(sb.Capacity);var sb2 = new StringBuilder(1000);Console.WriteLine(sb2.Capacity);
>>> Hello, C#!4220331000
Join and Concat
For assembling strings from collections or a small number of parts without StringBuilder.
Assemble strings with string.Join and string.Concat
string.Join inserts a separator between each element of a collection — the single most efficient way to build delimited output (CSV rows, log lines). string.Concat joins without a separator. For 2–5 known parts, the + operator is fine — the compiler optimizes small fixed concatenations into a single string.Concat call.
This example assembles strings with string.Join, string.Concat, and a small fixed + concatenation.
var items = Enumerable.Range(0, 5).Select(i => $"item_{i}");Console.WriteLine($"'{string.Join(", ", items)}'");Console.WriteLine($"'{string.Concat(Enumerable.Range(0, 5))}'");string first = "Hello";string last = "World";string full = first + " " + last;Console.WriteLine($"'{full}'");
For hot paths where allocation pressure matters, ReadOnlySpan<char> and string.Create avoid intermediate string objects entirely.
Slice strings without allocation using AsSpan
string.AsSpan() returns a ReadOnlySpan<char> — a stack-only view into the string’s character buffer with zero allocation. Many BCL methods (int.Parse, Regex, MemoryExtensions) accept spans directly, allowing parsing and inspection without creating substring copies.
This example slices a delimited row with AsSpan() inside a local parser to avoid substring allocations.
string.Create allocates exactly one string and lets you write directly into its buffer via a SpanAction<char, TState>. This avoids the temporary allocations that StringBuilder or concatenation would produce.
This example builds one string directly into its destination buffer with string.Create.
The System.Text.RegularExpressions namespace provides pattern matching, extraction, replacement, and splitting. Always use verbatim strings (@"") for patterns to avoid double-escaping backslashes.
Matching and capturing
Finding patterns in text and extracting matched groups.
Find matches with Regex.Match, Matches, and IsMatch
Regex
Regex.Match — returns the first match (check .Success)
Regex.Matches — returns all matches as MatchCollection
Use @"" verbatim strings to avoid double-escaping backslashes
Groups[0] is the full match; Groups[1..n] are capture groups
For simple Contains/StartsWith checks, string methods are faster
Regex code usually fails through silent assumptions or repeated setup cost
The two most common issues are reading match data that never succeeded and
recompiling the same pattern over and over in hot paths.
Missing success checks and repeated regex construction hide bugs and waste CPU
Not checking .Success before reading .Value — empty match is not null
Recompiling the same pattern in a loop — cache with new Regex()
Validate matches and reuse compiled patterns
Always check .Success before accessing match data, and cache compiled
Regex instances outside loops:
// Anti-pattern: no Success checkstring val = Regex.Match(text, @"\d+").Value; // returns "" if no match — silent bug// Correct: guard with Successvar m = Regex.Match(text, @"\d+");if (m.Success) Console.WriteLine(m.Value);// Anti-pattern: recompile every iterationforeach (var line in lines) Regex.IsMatch(line, @"\d{3}-\d{4}");// Correct: compile once, reusevar pat = new Regex(@"\d{3}-\d{4}", RegexOptions.Compiled);foreach (var line in lines) pat.IsMatch(line);
Regex.Match returns the first match; Regex.Matches returns all matches as a MatchCollection. Regex.IsMatch is a quick boolean check that avoids allocating match objects when you only need yes/no.
This example uses Regex.Match, Regex.Matches, and Regex.IsMatch to find one match, many matches, and a boolean result.
string text = "Contact us at support@email.com or sales@company.org. Call 123-456-7890 or 987-654-3210.";var match = Regex.Match(text, @"\d{3}-\d{3}-\d{4}");if (match.Success) Console.WriteLine($"{match.Value} at [{match.Index}:{match.Index + match.Length}]");var phones = Regex.Matches(text, @"\d{3}-\d{3}-\d{4}");var emails = Regex.Matches(text, @"[\w.+-]+@[\w-]+\.[\w.]+");Console.WriteLine(string.Join(", ", phones.Select(m => m.Value)));Console.WriteLine(string.Join(", ", emails.Select(m => m.Value)));foreach (Match m in phones) Console.WriteLine($" {m.Value} at [{m.Index}:{m.Index + m.Length}]");Console.WriteLine(Regex.IsMatch("12345", @"^\d+$"));Console.WriteLine(Regex.IsMatch("123a5", @"^\d+$"));
123-456-7890 at [59:71]123-456-7890, 987-654-3210support@email.com, sales@company.org. 123-456-7890 at [59:71] 987-654-3210 at [75:87]TrueFalse
Extract sub-matches with capture groups
Parentheses (...) in a pattern create numbered capture groups accessible via Groups[1], Groups[2], etc. (Groups[0] is always the full match). Named groups (?<name>...) use angle brackets (not P<> as in Python) and are accessed via Groups["name"].
This example extracts numbered and named capture groups from regex matches.
Transforming text with pattern-based replacement, splitting on patterns, and pre-compiling for performance.
Replace, split, and compile regex patterns
Regex.Replace substitutes matches — pass a string for static replacement, a MatchEvaluator lambda for dynamic transformation, or $1/$2 backreferences for group rearrangement. Regex.Split tokenizes on a pattern instead of a fixed delimiter. For patterns used repeatedly, instantiate new Regex(..., RegexOptions.Compiled) once — this precompiles the pattern to IL, avoiding re-parsing on each call.
This example replaces, splits, and reuses a compiled regex pattern.
Console.WriteLine(Regex.Replace(text, @"\d{3}-\d{3}-\d{4}", "***-***-****"));Console.WriteLine(Regex.Replace("price: 50, qty: 3", @"\d+", m => (int.Parse(m.Value) * 2).ToString()));Console.WriteLine(Regex.Replace("user@host", @"(\w+)@(\w+)", "$2/$1"));Console.WriteLine(string.Join(", ", Regex.Split("Hello World. How are you? Fine!", @"[.!?]\s*").Where(part => part.Length > 0)));Console.WriteLine(string.Join(", ", Regex.Split("a , b , c", @"\s*,\s*")));var phonePat = new Regex(@"\d{3}-\d{3}-\d{4}", RegexOptions.Compiled);Console.WriteLine(string.Join(", ", phonePat.Matches(text).Select(m => m.Value)));Console.WriteLine(phonePat.Replace(text, "REDACTED"));
Contact us at support@email.com or sales@company.org. Call ***-***-**** or ***-***-****.price: 100, qty: 6host/userHello World, How are you, Finea, b, c123-456-7890, 987-654-3210Contact us at support@email.com or sales@company.org. Call REDACTED or REDACTED.
Syntax reference
Quick-reference tables for regex syntax elements.
Regex syntax — characters, quantifiers, anchors, groups
A condensed reference for .NET regex syntax. All patterns apply to System.Text.RegularExpressions.
CHARACTERS. Any character (except newline)\d Digit [0-9] \D Non-digit\w Word char [a-zA-Z0-9_] \W Non-word\s Whitespace [ \t\n\r] \S Non-whitespace\b Word boundary \B Non-word boundaryQUANTIFIERS* 0 or more (greedy) *? 0 or more (lazy)+ 1 or more (greedy) +? 1 or more (lazy)? 0 or 1 (optional) ?? 0 or 1 (lazy){n} Exactly n {n,m} Between n and m{n,} n or more {n,m}? Between n and m (lazy)ANCHORS^ Start of string/line $ End of string/line\A Start of string only \Z End of string onlyGROUPS(...) Capture group (?:...) Non-capture group(?<name>...) Named group (C# uses <> not P<>)\1, \2 Backreference by number \k<name> By nameLOOKAROUND(?=...) Lookahead (positive) (?!...) Lookahead (negative)(?<=...) Lookbehind (positive) (?<!...) Lookbehind (negative)CHARACTER CLASSES[abc] Any of a, b, c [^abc] NOT a, b, c[a-z] Range a through z [a-zA-Z0-9] Alphanumeric| OR (alternation)
Options and flags
RegexOptions flags modify matching behavior. Combine multiple flags with bitwise OR (|).
Control matching with RegexOptions flags
IgnoreCase enables case-insensitive matching. Multiline makes ^ and $ match line boundaries instead of string boundaries. Singleline makes . match newline characters (these two are independent despite the confusing names). IgnorePatternWhitespace allows formatting patterns with whitespace and inline # comments for readability.
This example applies RegexOptions flags such as IgnoreCase, Multiline, and Singleline.
string text = "Hello\nworld\nHELLO";var ic = Regex.Matches(text, @"hello", RegexOptions.IgnoreCase);Console.WriteLine(string.Join(", ", ic.Select(m => m.Value)));var ml = Regex.Matches(text, @"^\w+", RegexOptions.Multiline);Console.WriteLine(string.Join(", ", ml.Select(m => m.Value)));Console.WriteLine(Regex.IsMatch(text, @"Hello.world", RegexOptions.Singleline));
Hello, HELLOHello, world, HELLOTrue
Write readable patterns with IgnorePatternWhitespace
IgnorePatternWhitespace ignores unescaped whitespace in the pattern and enables # comments. This makes complex patterns self-documenting. Combine flags with bitwise OR to apply multiple options.
This example formats a verbose regex with IgnorePatternWhitespace and then prints the captured phone number.
var pattern = new Regex(@" (\d{3}) # area code [-.] # separator (\d{3}) # first 3 digits [-.] # separator (\d{4}) # last 4 digits", RegexOptions.IgnorePatternWhitespace);var m2 = pattern.Match("Call 123-456-7890");if (m2.Success) Console.WriteLine($"{m2.Groups[1]}-{m2.Groups[2]}-{m2.Groups[3]}");
123-456-7890
This example combines IgnoreCase and Multiline flags to match multiple line starts.
email : ^[\w.+-]+@[\w-]+\.[\w.]+$URL : https?://[\w./\-?=&#]+IPv4 : \b\d{1,3}(\.\d{1,3}){3}\bdate YYYY-MM-DD : \d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])time HH:MM : (?:[01]\d|2[0-3]):[0-5]\dhex color : ^#[0-9a-fA-F]{6}$phone US : \(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}zip code US : \d{5}(-\d{4})?strong password : ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{8,}$
Source-generated regex (.NET 7+)
The [GeneratedRegex] attribute replaces RegexOptions.Compiled with build-time source generation — faster startup, AOT-compatible, and trim-safe.
Use GeneratedRegex for compile-time regex generation
Decorate a partial method returning Regex with [GeneratedRegex]. The source generator emits optimized matching code at compile time, eliminating the runtime IL-emit cost of RegexOptions.Compiled. The generated code is also compatible with Native AOT and trimming, which Compiled is not.
When to use GeneratedRegex vs Compiled
Use [GeneratedRegex] for all patterns known at compile time — it is strictly better than Compiled in every dimension (startup, throughput, AOT). Reserve new Regex(..., RegexOptions.Compiled) only when the pattern is constructed dynamically at runtime. Analyzer SYSLIB1045 automatically flags existing Regex usages that can be converted, with a one-click fixer in Visual Studio.
Declare and call a source-generated regex
The containing class must be partial so the source generator can emit the matching implementation. The [GeneratedRegex] attribute takes the pattern and optional RegexOptions. Call the generated method to get a cached Regex instance, then use it like any other Regex. The example below includes a manual fallback implementation so the snippet remains executable in script hosts that do not run source generators.
This example declares a [GeneratedRegex] partial method and calls it through a script-safe fallback implementation.
partial class MyRegexHelper{ [GeneratedRegex(@"\d{3}-\d{3}-\d{4}", RegexOptions.IgnoreCase)] public static partial Regex PhonePattern(); // Script-host fallback: a normal project build would get this body from the source generator. public static partial Regex PhonePattern() => new(@"\d{3}-\d{3}-\d{4}", RegexOptions.IgnoreCase);}string phoneText = "Call 123-456-7890";var match = MyRegexHelper.PhonePattern().Match(phoneText);Console.WriteLine(match.Value);
123-456-7890
C# String Workloads
Applicability
Production string processing — C#‘s static typing, Span<char>, and StringBuilder provide predictable performance for high-throughput text handling in services and pipelines.
Zero-allocation parsing — ReadOnlySpan<char> with int.Parse(span) and string.AsSpan() enables parsing without substring allocations — critical for hot paths processing millions of records.
Complex regex with compile-time safety — [GeneratedRegex] (C# 11/.NET 7+) produces optimized, AOT-compatible matching code with compile-time pattern validation.
Culture-aware string operations — StringComparison enum and CultureInfo provide fine-grained control over case folding, sorting, and collation rules.
Windows and .NET ecosystem — native integration with ASP.NET, EF Core, and Azure SDKs where string APIs are designed for the platform.
Limitations
Ad-hoc text exploration — C#‘s ceremony makes quick regex experimentation slower than Python’s REPL. Prototype patterns in Python, then port to C#.
Dynamic scripting — Python’s split()/join() with list comprehensions is more concise for one-off text transformations.
No built-in translate() equivalent — C# lacks Python’s str.maketrans/str.translate for single-pass multi-character substitution. Use Regex.Replace or manual StringBuilder loops.
UTF-16 surrogate pair complexity — C# strings are UTF-16 internally. Characters outside the Basic Multilingual Plane (emoji, rare scripts) are stored as surrogate pairs, making string[i] return half a character. Use StringInfo.GetTextElementEnumerator() for correct grapheme iteration.
Common Traps and Safe Patterns
Build Large Strings with the Right Tool
Large string assembly needs an explicit buffering strategy
Repeated concatenation looks harmless in small examples but scales badly under
real workloads.
String concatenation in loops is O(n^2)
Each += allocates a new string and copies all previous content. For large
iteration counts, StringBuilder is dramatically faster.
Use a builder or join strategy
Use new StringBuilder(estimatedCapacity) for loops. Use string.Join() for
collections. Reserve + for a few fixed parts that the compiler can fold
into string.Concat.
Treat Interpolated Commands as Injection Risks
String interpolation becomes dangerous the moment text crosses into a command language
SQL, shells, and similar interpreters do not distinguish trusted values from
concatenated text unless you give them structured parameters.
Interpolated SQL and shell commands are injection-prone
$"SELECT * FROM users WHERE id = {userId}" is vulnerable to SQL injection,
and the same pattern is unsafe for shell command construction.
Pass structured parameters instead of interpolated text
SQL: command.Parameters.AddWithValue("@id", userId). Shell: pass arguments
as arrays or structured argument objects to Process.Start().
Specify Comparison Semantics Explicitly
String equality has to state whether case and culture matter
Relying on == bakes in ordinal case-sensitive semantics whether or not that
matches the domain.
== hides the comparison policy
== is always ordinal case-sensitive. Comparing user input or file paths
without specifying OrdinalIgnoreCase causes false negatives.
Call comparison APIs with an explicit StringComparison
Use string.Equals(a, b, StringComparison.OrdinalIgnoreCase) for
case-insensitive matching. For culture-aware sorting or display comparisons,
use the relevant CurrentCulture option instead.
Reuse Regex Compilation on Repeated Paths
Repeated regex use should not pay the parse cost on every call
If the same pattern runs in a loop or hot path, startup cost becomes part of
the steady-state cost.
Default regex construction reparses the pattern repeatedly
new Regex(pattern) interprets the pattern on every call. For repeated use,
this is slower than generated or compiled variants.
Use generated or compiled regex for repeated patterns
Use [GeneratedRegex] on .NET 7+ for compile-time-known patterns. Use
new Regex(pattern, RegexOptions.Compiled) for dynamic patterns that are
still reused.
C# Strings Recommendations
Use StringBuilder for any string construction involving loops or more than ~5 concatenations.
Use Span<char> / ReadOnlySpan<char> for hot-path parsing — avoids Substring() allocations.
Use [GeneratedRegex] for all compile-time-known patterns — better performance, AOT-compatible, and the analyzer SYSLIB1045 flags opportunities.
Always specify StringComparison in Equals(), IndexOf(), StartsWith(), and Compare() — never rely on the default.
Prefer string.IsNullOrWhiteSpace() over string.IsNullOrEmpty() — whitespace-only strings are almost always invalid input.
Use raw string literals ("""...""", C# 11+) for JSON templates, SQL, regex patterns, and any string with heavy escaping.
Use string.Create() (.NET 5+) for advanced zero-allocation string construction with a SpanAction<char> callback.
Avoid string.Format() in new code — prefer $"" interpolation. Use string.Format() only when the template is a runtime variable.
C# Strings Troubleshooting
NullReferenceException on string methods
string instance methods such as .Length, .Trim(), and .Replace() throw on null. Guard with string.IsNullOrEmpty() or a null-conditional fallback before you call the method.
This example checks a nullable string before trimming it.
string indexing is 0-based and stops at s.Length - 1. Check the bounds first or slice with s.AsSpan() when you only need a safe range.
This example guards the index and then uses AsSpan() for a safe slice.
string s = "hello";int index = 7;Console.WriteLine(index < s.Length ? s[index] : '?');Console.WriteLine(s.AsSpan(1, 3).ToString());
?ell
== misses case-insensitive matches
== is ordinal and case-sensitive. Use string.Equals(a, b, StringComparison.OrdinalIgnoreCase) when the comparison should ignore case.
This example compares the same two values with both operators.
string a = "Hello";string b = "hello";Console.WriteLine(a == b);Console.WriteLine(string.Equals(a, b, StringComparison.OrdinalIgnoreCase));
FalseTrue
Groups["name"] is empty
A named group only carries text when it participated in the match. Check group.Success before reading .Value.
This example shows a named group that did not match any digits.
using System.Text.RegularExpressions;var match = Regex.Match("ID=", @"ID=(?<id>\d+)");var group = match.Groups["id"];Console.WriteLine(group.Success);Console.WriteLine($"'{group.Value}'");
False''
ArgumentException means the regex is invalid
Regex syntax errors surface as ArgumentException when the pattern is parsed. Validate the pattern earlier, or rely on [GeneratedRegex] for compile-time patterns.
This example catches the parse error from an invalid pattern.
using System.Text.RegularExpressions;try{ _ = new Regex(@"(\d+"); Console.WriteLine("parsed");}catch (ArgumentException ex){ Console.WriteLine(ex.GetType().Name);}
ArgumentException
StringBuilder separators should not trail
Appending the separator after every item creates an extra suffix. Use string.Join() or add the separator only after the first item.
This example uses string.Join() to avoid a trailing separator.
A char is one UTF-16 code unit, not always one visible character. Use StringInfo.GetTextElementEnumerator() when you need grapheme-safe iteration.
This example enumerates text elements instead of raw UTF-16 code units.
using System.Globalization;string text = "A🙂B";var enumerator = StringInfo.GetTextElementEnumerator(text);while (enumerator.MoveNext()){ Console.WriteLine(enumerator.GetTextElement());}
A🙂B
Encoding.UTF8 should be explicit
Default encoding can differ from the file’s actual bytes. Specify Encoding.UTF8 when you read or write text that must round-trip cleanly.
This example encodes and decodes text with Encoding.UTF8.
using System.Text;byte[] bytes = Encoding.UTF8.GetBytes("café");Console.WriteLine(Encoding.UTF8.GetString(bytes));
café
Regex.Replace() slows down in tight loops
A repeatedly parsed pattern wastes work. Cache the regex or use [GeneratedRegex] for patterns that are known at compile time.
This example reuses a compiled regex to replace phone numbers.
using System.Text.RegularExpressions;var phonePat = new Regex(@"\d{3}-\d{3}-\d{4}", RegexOptions.Compiled);Console.WriteLine(phonePat.Replace("Call 123-456-7890", "REDACTED"));
Call REDACTED
string.Format() needs matching placeholder indices
FormatException usually means the composite-format indices do not line up with the argument list. Recheck the placeholders or switch to interpolation when the template is static.