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
| Symbol | Znaczenie | Przykład |
|---|---|---|
\d | cyfra | \d{3} = dokładnie 3 cyfry |
\w | znak słowa (litera, cyfra, _) | \w+ = jeden lub więcej |
\s | biał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 (Contains, StartsWith, Split) — 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# (IsMatch, Match, Matches, Replace) 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.
🚀 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.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
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ę →