This article covers three string operations: regular-expression pattern matching with System.Text.RegularExpressions.Regex, allocation-free search over System.ReadOnlySpan, and choosing a System.StringComparison value for correct, fast comparisons.
Find specific text by using regular expressions
The System.Text.RegularExpressions.Regex class searches strings for patterns rather than fixed substrings. The static System.Text.RegularExpressions.Regex.IsMatch method takes the input string, a pattern, and optional System.Text.RegularExpressions.RegexOptions flags.
The following example searches each sentence for the word the or their, case insensitive. The pattern the(ir)?\s matches the optionally followed by ir, then a whitespace character:
| Pattern | Meaning |
|---|---|
the |
match the literal text the |
(ir)? |
match 0 or 1 occurrence of ir |
\s |
match a whitespace character |
sample · csharpdocs/csharp/language-reference/builtin-types/snippets/string-operations/Program.csRegexPattern
using System.Text.RegularExpressions;
namespace StringOperations;
public static class Program
{
public static void Main()
{
RegexPattern();
Console.WriteLine();
RegexValidate();
Console.WriteLine();
SpanSearch();
}
private static void RegexPattern()
{
// <RegexPattern>
string[] sentences =
[
"Put the water over there.",
"They're quite thirsty.",
"Their water bottles broke."
];
string pattern = @"the(ir)?\s";
foreach (string s in sentences)
{
Console.Write($"{s,28}");
if (Regex.IsMatch(s, pattern, RegexOptions.IgnoreCase))
{
Console.WriteLine($" (match for '{pattern}' found)");
}
else
{
Console.WriteLine();
}
}
// </RegexPattern>
}
private static void RegexValidate()
{
// <RegexValidate>
string[] numbers =
[
"123-555-0190",
"444-234-22450",
"690-555-0178",
"146-893-232",
"146-555-0122",
"4007-555-0111",
"407-555-0111",
"407-2-5555",
"407-555-8974",
"407-2ab-5555",
"690-555-8148",
"146-893-232-"
];
string pattern = """^\d{3}-\d{3}-\d{4}$""";
foreach (string s in numbers)
{
Console.Write($"{s,14}");
Console.WriteLine(Regex.IsMatch(s, pattern) ? " - valid" : " - invalid");
}
// </RegexValidate>
}
private static void SpanSearch()
{
// <SpanSearch>
ReadOnlySpan<char> input = "key1=alpha;key2=beta;key3=gamma".AsSpan();
ReadOnlySpan<char> needle = "key2=".AsSpan();
int start = input.IndexOf(needle);
if (start >= 0)
{
ReadOnlySpan<char> rest = input[(start + needle.Length)..];
int end = rest.IndexOf(';');
ReadOnlySpan<char> value = end >= 0 ? rest[..end] : rest;
Console.WriteLine($"key2 = {value}");
}
// => key2 = beta
// </SpanSearch>
}
}Validate strings against a pattern
To check whether an entire input matches a shape, anchor the pattern with ^ and $. The following example validates that each string is a US-style telephone number: three digits, three digits, four digits, separated by dashes:
| Pattern | Meaning |
|---|---|
^ |
match the beginning of the string |
\d{3} |
match exactly three digit characters |
- |
match a literal - character |
\d{4} |
match exactly four digit characters |
$ |
match the end of the string |
sample · csharpdocs/csharp/language-reference/builtin-types/snippets/string-operations/Program.csRegexValidate
using System.Text.RegularExpressions;
namespace StringOperations;
public static class Program
{
public static void Main()
{
RegexPattern();
Console.WriteLine();
RegexValidate();
Console.WriteLine();
SpanSearch();
}
private static void RegexPattern()
{
// <RegexPattern>
string[] sentences =
[
"Put the water over there.",
"They're quite thirsty.",
"Their water bottles broke."
];
string pattern = @"the(ir)?\s";
foreach (string s in sentences)
{
Console.Write($"{s,28}");
if (Regex.IsMatch(s, pattern, RegexOptions.IgnoreCase))
{
Console.WriteLine($" (match for '{pattern}' found)");
}
else
{
Console.WriteLine();
}
}
// </RegexPattern>
}
private static void RegexValidate()
{
// <RegexValidate>
string[] numbers =
[
"123-555-0190",
"444-234-22450",
"690-555-0178",
"146-893-232",
"146-555-0122",
"4007-555-0111",
"407-555-0111",
"407-2-5555",
"407-555-8974",
"407-2ab-5555",
"690-555-8148",
"146-893-232-"
];
string pattern = """^\d{3}-\d{3}-\d{4}$""";
foreach (string s in numbers)
{
Console.Write($"{s,14}");
Console.WriteLine(Regex.IsMatch(s, pattern) ? " - valid" : " - invalid");
}
// </RegexValidate>
}
private static void SpanSearch()
{
// <SpanSearch>
ReadOnlySpan<char> input = "key1=alpha;key2=beta;key3=gamma".AsSpan();
ReadOnlySpan<char> needle = "key2=".AsSpan();
int start = input.IndexOf(needle);
if (start >= 0)
{
ReadOnlySpan<char> rest = input[(start + needle.Length)..];
int end = rest.IndexOf(';');
ReadOnlySpan<char> value = end >= 0 ? rest[..end] : rest;
Console.WriteLine($"key2 = {value}");
}
// => key2 = beta
// </SpanSearch>
}
}For the full pattern syntax, see Regular expression language - quick reference.
Choose between string methods and regular expressions
string methods and Regex solve overlapping problems. Prefer string methods when the text you're searching for is a literal value, a known prefix or suffix, or a fixed delimiter. They're simpler to read and faster, because they don't pay the cost of compiling and executing a pattern. Reach for Regex when the search target is a shape, such as alternations, optional groups, repeated character classes, or anchored validation. As a rule of thumb, if you can write the search as one or two string.Contains / StartsWith / IndexOf calls, do so.
Search using ReadOnlySpan<char>
When you parse large inputs or run a search on a hot path, the per-call allocations of string.Substring and string.Split can dominate. ReadOnlySpan<char> gives you a view over an existing string (or array, or stack buffer) without copying, and System.MemoryExtensions provides span-based equivalents of the common string methods, including System.MemoryExtensions.IndexOf:
sample · csharpdocs/csharp/language-reference/builtin-types/snippets/string-operations/Program.csSpanSearch
using System.Text.RegularExpressions;
namespace StringOperations;
public static class Program
{
public static void Main()
{
RegexPattern();
Console.WriteLine();
RegexValidate();
Console.WriteLine();
SpanSearch();
}
private static void RegexPattern()
{
// <RegexPattern>
string[] sentences =
[
"Put the water over there.",
"They're quite thirsty.",
"Their water bottles broke."
];
string pattern = @"the(ir)?\s";
foreach (string s in sentences)
{
Console.Write($"{s,28}");
if (Regex.IsMatch(s, pattern, RegexOptions.IgnoreCase))
{
Console.WriteLine($" (match for '{pattern}' found)");
}
else
{
Console.WriteLine();
}
}
// </RegexPattern>
}
private static void RegexValidate()
{
// <RegexValidate>
string[] numbers =
[
"123-555-0190",
"444-234-22450",
"690-555-0178",
"146-893-232",
"146-555-0122",
"4007-555-0111",
"407-555-0111",
"407-2-5555",
"407-555-8974",
"407-2ab-5555",
"690-555-8148",
"146-893-232-"
];
string pattern = """^\d{3}-\d{3}-\d{4}$""";
foreach (string s in numbers)
{
Console.Write($"{s,14}");
Console.WriteLine(Regex.IsMatch(s, pattern) ? " - valid" : " - invalid");
}
// </RegexValidate>
}
private static void SpanSearch()
{
// <SpanSearch>
ReadOnlySpan<char> input = "key1=alpha;key2=beta;key3=gamma".AsSpan();
ReadOnlySpan<char> needle = "key2=".AsSpan();
int start = input.IndexOf(needle);
if (start >= 0)
{
ReadOnlySpan<char> rest = input[(start + needle.Length)..];
int end = rest.IndexOf(';');
ReadOnlySpan<char> value = end >= 0 ? rest[..end] : rest;
Console.WriteLine($"key2 = {value}");
}
// => key2 = beta
// </SpanSearch>
}
}Span-based search avoids allocations because the slices (input[start..], rest[..end]) are simply windows over the original characters. The same approach scales to parsing key-value lists, headers, and other delimited text without ever calling Substring.
Performance considerations for StringComparison
Most string instance methods have overloads that accept a System.StringComparison value. Methods such as System.String.Equals(System.String) default to ordinal, but System.String.Compare(System.String,System.String) and System.String.IndexOf(System.String) default to current culture. This difference matters in two ways:
- Speed. Ordinal comparison is a byte-for-byte test that runs in tight, vectorized loops. Culture-aware comparison consults a sort table, walks combining characters, and applies locale-specific rules. For the same input, it can be an order of magnitude slower.
- Correctness. Culture-aware comparison can fold characters that you don't expect (Turkish
i/I, Germanßtoss, ligatures). This behavior is right for sorting names a user sees but wrong for parsing identifiers, paths, or protocol tokens.
For machine-defined text, such as file names, URLs, HTTP headers, identifiers, and configuration keys, pass System.StringComparison.Ordinal or System.StringComparison.OrdinalIgnoreCase explicitly. Reserve culture-aware values for natural-language text shown to users. For comprehensive guidance, see Best practices for comparing strings in .NET.