C#idiomatic C#
string-operationscsharp/language-reference/builtin-types

Language

String operations: Pattern matching, performance, and span-based search

Learn how to apply regular-expression patterns to strings, search strings with ReadOnlySpan<char>, and pick the right StringComparison for performance.

  • page csharp/language-reference/builtin-types/string-operations
  • updated 05/21/2026

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

csharp
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

csharp
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

csharp
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 ß to ss, 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.

See also