🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

Regex w C# — wyrażenia regularne na przykładach, z pułapką ReDoS

bool WygladaJakEmailZle(string tekst)
{
    int atIndex = tekst.IndexOf('@');
    if (atIndex <= 0) return false;
    int dotIndex = tekst.IndexOf('.', atIndex);
    return dotIndex > atIndex + 1 && dotIndex < tekst.Length - 1;
    // a co z dwoma @, spacjami, znakami niedozwolonymi w emailu?
}

Ręczne parsowanie wzorców tekstowych przez IndexOf/Substring szybko robi się kruche — każdy nowy przypadek brzegowy to kolejny if. Wyrażenia regularne (regex) opisują wzorzec deklaratywnie, jednym ciągiem znaków, zamiast proceduralnie krok po kroku.

Podstawowa składnia — ściągawka

SymbolZnaczeniePrzykład
\dcyfra\d{3} = dokładnie 3 cyfry
\wznak słowa (litera, cyfra, _)\w+ = jeden lub więcej
\sbiały znak (spacja, tab)
.dowolny znak
+ * ?jeden-lub-więcej / zero-lub-więcej / opcjonalny
^ $początek / koniec tekstu
[abc]jeden ze znaków w nawiasie[A-Za-z] = dowolna litera
(...)grupa (do wyodrębnienia fragmentu)

Regex w C# — podstawowe metody

using System.Text.RegularExpressions;

string tekst = "Kontakt: jan.kowalski@firma.pl, tel. 501-234-567";

bool zawieraEmail = Regex.IsMatch(tekst, @"\w+@\w+\.\w+");        // true/false
Match dopasowanie = Regex.Match(tekst, @"\w+@\w+\.\w+");           // pierwsze dopasowanie
Console.WriteLine(dopasowanie.Value);                              // "jan.kowalski@firma.pl"

MatchCollection wszystkie = Regex.Matches(tekst, @"\d{3}-\d{3}-\d{3}"); // wszystkie dopasowania
string oczyszczony = Regex.Replace(tekst, @"\d", "•");             // zamiana wszystkich cyfr

Zwróć uwagę na @"..." — verbatim string literal. Bez niego każdy \ w wzorcu trzeba by podwoić (\\d zamiast \d), bo \ to też znak specjalny w zwykłych stringach C#. Prefiks @ wyłącza to escapowanie — praktycznie zawsze używaj go przy wzorcach regex.

Grupy — wyodrębnianie fragmentów, nie tylko sprawdzanie dopasowania

string wpis = "Cena: 149.99 PLN";
Match m = Regex.Match(wpis, @"Cena: (\d+\.\d+) (\w+)");

if (m.Success)
{
    string kwota = m.Groups[1].Value; // "149.99" — pierwsza grupa w nawiasach
    string waluta = m.Groups[2].Value; // "PLN" — druga grupa
}

Grupy w nawiasach (...) pozwalają wyciągnąć konkretne fragmenty dopasowanego tekstu, nie tylko potwierdzić, że wzorzec pasuje. Numeracja grup zaczyna się od 1 (grupa 0 to całe dopasowanie).

Grupy nazwane — czytelniejsze niż numery

Match m = Regex.Match(wpis, @"Cena: (?<kwota>\d+\.\d+) (?<waluta>\w+)");

string kwota = m.Groups["kwota"].Value;   // "149.99" — po nazwie, nie po numerze
string waluta = m.Groups["waluta"].Value; // "PLN"

(?<nazwa>...) nadaje grupie nazwę zamiast pozycyjnego numeru — kod czytający m.Groups["kwota"] jest czytelniejszy i odporniejszy na pomyłkę, gdy ktoś doda albo usunie grupę w środku wzorca i wszystkie numery się przesuną.

Regex.Replace z odwołaniem do grup

string data = "2026-07-13";
string sformatowana = Regex.Replace(data, @"(\d{4})-(\d{2})-(\d{2})", "$3.$2.$1");
Console.WriteLine(sformatowana); // "13.07.2026"

$1$2$3 w tekście zamiennym odwołują się do dopasowanych grup — pozwala to przestawiać, łączyć albo częściowo zamieniać dopasowany fragment, nie tylko usuwać go w całości.

Regex.Split i opcje dopasowania

string tekst = "jabłko, gruszka;  banana   pomarańcza";
string[] owoce = Regex.Split(tekst, @"[,;\s]+"); // dzieli po przecinku, średniku LUB spacjach naraz
// ["jabłko", "gruszka", "banana", "pomarańcza"]

bool ignorujWielkosc = Regex.IsMatch("WARSZAWA", "warszawa", RegexOptions.IgnoreCase); // true

Regex.Split radzi sobie z sytuacją, w której zwykły string.Split(',') by nie wystarczył — tu dzielimy jednocześnie po przecinku, średniku i dowolnej liczbie białych znaków, w jednym wzorcu. RegexOptions.IgnoreCase to najczęściej używana opcja dopasowania — inne warte znajomości to RegexOptions.Multiline (zmienia znaczenie ^/$ na początek/koniec każdej linii, nie całego tekstu).

Skompilowany regex — wydajność w gorącej pętli

// Klasyczne podejście — wzorzec kompilowany przy każdym wywołaniu
bool wynik = Regex.IsMatch(tekst, @"\d{3}-\d{3}-\d{3}");

// GeneratedRegex (C# 11+ / .NET 7+) — wzorzec generowany raz, w trakcie kompilacji
public partial class Walidatory
{
    [GeneratedRegex(@"\d{3}-\d{3}-\d{3}")]
    public static partial Regex NumerTelefonu();
}

bool wynik2 = Walidatory.NumerTelefonu().IsMatch(tekst); // szybsze przy wielokrotnym użyciu

Jeśli ten sam wzorzec sprawdzasz tysiące razy (np. walidacja każdego wiersza w imporcie CSV), atrybut [GeneratedRegex] generuje wyspecjalizowany, skompilowany kod dopasowujący wzorzec już na etapie budowania projektu — zauważalnie szybsze niż domyślne, interpretowane dopasowanie przy każdym wywołaniu.

Pułapka: katastrofalny backtracking (ReDoS)

// ❌ Wzorzec podatny na katastrofalny backtracking
string zlyWzorzec = @"^(\w+\s?)+$";

string zlosliwyTekst = new string('a', 30) + "!"; // 30 liter + znak, który NIE pasuje

bool wynik = Regex.IsMatch(zlosliwyTekst, zlyWzorzec); // może wisieć SEKUNDY albo MINUTY

Niektóre wzorce z zagnieżdżonymi kwantyfikatorami ((\w+\s?)+) mają katastrofalną złożoność czasową dla pewnych wejść — silnik regex próbuje wykładniczo wiele sposobów dopasowania, zanim stwierdzi porażkę. To nie tylko problem wydajności — to realna podatność bezpieczeństwa (ReDoS, Regular Expression Denial of Service): jeśli wzorzec waliduje dane wejściowe od użytkownika (np. pole formularza), ktoś może celowo wysłać tekst, który zawiesi Twój serwer na pojedynczym żądaniu.

Obrona: zawsze ustawiaj limit czasu na dopasowanie danych pochodzących z zewnątrz:

var regex = new Regex(wzorzec, RegexOptions.None, TimeSpan.FromMilliseconds(500));
try
{
    bool wynik = regex.IsMatch(tekstOdUzytkownika);
}
catch (RegexMatchTimeoutException)
{
    // wzorzec nie dopasował się w rozsądnym czasie — potraktuj jako niepoprawne dane
}

TimeSpan przekazany do konstruktora Regex sprawia, że silnik rzuci wyjątek zamiast wisieć w nieskończoność — dla każdego regexu walidującego dane od użytkownika to nie jest opcjonalne zabezpieczenie, to standard.

Kiedy NIE używać regex

// ❌ Nadmiarowe — regex do sprawdzenia zwykłego podciągu
bool zawiera = Regex.IsMatch(tekst, "błąd");

// ✅ Prostsze, szybsze, czytelniejsze
bool zawiera2 = tekst.Contains("błąd");

Regex to potężne, ale cięższe narzędzie niż zwykłe metody string (ContainsStartsWithSplit) — dla dosłownego dopasowania podciągu regex jest zarówno wolniejszy, jak i mniej czytelny niż wbudowana metoda, która robi dokładnie to samo. Sięgaj po regex, gdy potrzebujesz wzorca (zakresy znaków, powtórzenia, alternatywy), nie dosłownego tekstu.

Podsumowanie

Regex w C# (IsMatchMatchMatchesReplace) opisuje wzorce tekstowe deklaratywnie, z grupami (najlepiej nazwanymi) do wyodrębniania fragmentów. Dla wzorców używanych wielokrotnie sięgaj po [GeneratedRegex] zamiast domyślnej, interpretowanej wersji. Najważniejsza pułapka, o której się nie mówi na kursach: źle skonstruowany wzorzec z zagnieżdżonymi kwantyfikatorami może zawiesić aplikację na pojedynczym złośliwym wejściu (ReDoS) — dla każdego regexu walidującego dane od użytkownika ustawiaj TimeSpan jako limit czasu dopasowania.

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 Co dalej?

Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.

Dodaj komentarz

czytanie to początek

Zamień wiedzę w umiejętności

Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.

Pobieram roadmapę →