Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Regulární výraz neboli regex je řetězec, který vývojářům umožňuje vyjádřit vzor, což představuje běžný způsob, jakým hledat text a extrahovat výsledky jako podmnožinu hledaného řetězce. V .NET se System.Text.RegularExpressions obor názvů používá k definování Regex instancí a statických metod a porovnávání s uživatelsky definovanými vzory. V tomto článku se dozvíte, jak pomocí generování zdrojového kódu generovat Regex instance pro optimalizaci výkonu.
Poznámka:
Pokud je to možné, místo kompilování regulárních výrazů pomocí RegexOptions.Compiled možnosti používejte zdrojové generované regulární výrazy. Generování zdrojového kódu může pomoci vaší aplikaci rychleji se spouštět, běžet efektivněji a být snadněji optimalizovatelná. Informace o tom, kdy je možné generování zdroje, najdete v tématu Kdy ji použít.
Kompilované regulární výrazy
Když píšete new Regex("somepattern"), stane se pár věcí. Zadaný vzor se analyzuje, a to jak k zajištění platnosti vzoru, tak k jeho transformaci na interní strom, který představuje parsovaný regulární výraz. Strom se pak optimalizuje různými způsoby a transformuje model na funkčně ekvivalentní variantu, která se dá efektivněji spustit. Strom se zapíše do podoby, kterou lze interpretovat jako řadu operačních kódů a operandů, které poskytují pokyny interpretačnímu stroji regulárních výrazů, jak provádět shodu. Když se provede porovnání, interpret jednoduše prochází těmito instrukcemi a zpracovává je ve vztahu ke vstupnímu textu. Při vytváření nové instance Regex nebo volání jedné ze statických metod na Regex je interpret použit jako výchozí motor.
Při zadání RegexOptions.Compiled se provede veškerá stejná pracovní činnost při sestavování. Výsledné instrukce se dále transformují kompilátorem založeným na reflexi na instrukce IL, které jsou zapsány do několika DynamicMethod objektů. Při provedení shody jsou tyto DynamicMethod metody vyvolány. Tento IL v podstatě dělá přesně to, co by dělal interpret, pouze je specializovaný na přesný vzor, který se zpracovává. Pokud například vzor obsahuje [ac], interpret vidí kód operace, který říká, že "porovná vstupní znak na aktuální pozici se sadou specifikovanou v tomto popisu sady". Zatímco zkompilovaná IL by obsahovala kód, který efektivně říká: "Porovnej vstupní znak na aktuální pozici s 'a' nebo 'c'". Tato speciální velikost a schopnost provádět optimalizace na základě znalostí modelu jsou některé z hlavních důvodů, které určují RegexOptions.Compiled mnohem rychleji odpovídající propustnost než interpret.
Existuje několik nevýhod RegexOptions.Compiled. Nejvýraznější je, že je nákladné sestavit. Nejenže jsou všechny náklady uhrazeny jako za interpretera, ale pak je potřeba zkompilovat výsledný RegexNode strom a vygenerované opkódy/operandy do IL, což přidává značné náklady. Vygenerované IL musí být dále zkompilovány JIT při prvním použití, což vede k ještě větším nákladům při spuštění.
RegexOptions.Compiled představuje základní kompromis mezi režií při prvním použití a režijními náklady při každém následném použití. Použití System.Reflection.Emit také inhibuje použití RegexOptions.Compiled v určitých prostředích; některé operační systémy neumožňují dynamicky generovaný kód spustit a v takových systémech Compiled se stává no-op.
Generování zdrojového kódu
.NET 7 zavedl nový RegexGenerator generátor zdrojů.
Generátor zdroje je komponenta, která se připojuje k kompilátoru a rozšiřuje kompilační jednotku o další zdrojový kód. Sada .NET SDK obsahuje generátor zdroje, který rozpozná GeneratedRegexAttribute atribut částečné metody, která vrací Regex. Počínaje rozhraním .NET 9 lze atribut použít také na částečné vlastnosti. Generátor kódu poskytuje implementaci této metody nebo vlastnosti, která obsahuje veškerou logiku pro Regex. Dříve jste například mohli napsat kód takto:
private static readonly Regex s_abcOrDefGeneratedRegex =
new(pattern: "abc|def",
options: RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static void EvaluateText(string text)
{
if (s_abcOrDefGeneratedRegex.IsMatch(text))
{
// Take action with matching text
}
}
Pokud chcete použít generátor zdroje, přepíšete předchozí kód následujícím způsobem:
[GeneratedRegex("abc|def", RegexOptions.IgnoreCase, "en-US")]
private static partial Regex AbcOrDefGeneratedRegex();
private static void EvaluateText(string text)
{
if (AbcOrDefGeneratedRegex().IsMatch(text))
{
// Take action with matching text
}
}
Počínaje verzí .NET 9 můžete také použít GeneratedRegexAttribute na částečnou vlastnost místo částečné metody. Tato možnost je povolena podporou C# 13 pro částečné vlastnosti. Následující příklad ukazuje ekvivalent vlastnosti:
[GeneratedRegex("abc|def", RegexOptions.IgnoreCase, "en-US")]
private static partial Regex AbcOrDefGeneratedRegexProperty { get; }
private static void EvaluateText(string text)
{
if (AbcOrDefGeneratedRegexProperty.IsMatch(text))
{
// Take action with matching text
}
}
Návod
Příznak RegexOptions.Compiled je ignorován zdrojovým generátorem, takže není potřeba ve zdrojové verzi vygenerované zdrojem.
Vygenerovaná implementace AbcOrDefGeneratedRegex() podobně ukládá jednu Regex instanci do mezipaměti, takže ke zpracování kódu není potřeba žádné další ukládání do mezipaměti.
Následující obrázek je snímek obrazovky instance generované zdrojem uložené v mezipaměti pro podtřídu Regex, kterou vydává generátor zdrojů:
Ale jak je vidět, nedělá jen new Regex(...). Zdrojový generátor generuje kód v jazyce C#, který je vlastní implementací odvozenou od Regex, s logikou podobnou té, kterou RegexOptions.Compiled generuje v IL. Získáte všechny výhody propustnosti RegexOptions.Compiled a výhody Regex.CompileToAssembly spuštění bez složitosti CompileToAssembly. Zdroj, který je emitován, je součástí projektu, což znamená, že je také snadno zobrazitelný a laditelný.
Návod
V sadě Visual Studio klikněte pravým tlačítkem na deklaraci částečné metody nebo vlastnosti a vyberte Přejít na definici. Nebo, případně, vyberte uzel projektu v Průzkumník řešení, poté rozbalte Závislosti>Analyzers>System.Text.RegularExpressions.Generator>>, aby se zobrazil vygenerovaný C# kód z tohoto generátoru regulárních výrazů.
V něm můžete nastavit zarážky, můžete je procházet a můžete ho použít jako výukový nástroj, abyste přesně pochopili, jak modul regulárních výrazů zpracovává váš vzor se vstupem. Generátor dokonce generuje komentáře se třemi lomítky (XML), aby pomohly učinit výraz srozumitelným na první pohled a tam, kde se používá.
Uvnitř generovaných souborů ze zdroje
S rozhraním .NET 7 se zdrojový generátor i RegexCompiler téměř zcela přepsal, v podstatě se změnila struktura generovaného kódu. Tento přístup byl rozšířen tak, aby zpracovával všechny konstrukce (s jedním upozorněním) a jak RegexCompiler, tak zdrojový generátor se stále převážně mapují v poměru 1:1 podle nového přístupu. Představte si výstup generátoru zdroje pro jednu z primárních funkcí z výrazu abc|def :
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
int pos = base.runtextpos;
int matchStart = pos;
ReadOnlySpan<char> slice = inputSpan.Slice(pos);
// Match with 2 alternative expressions, atomically.
{
if (slice.IsEmpty)
{
return false; // The input didn't match.
}
switch (slice[0])
{
case 'A' or 'a':
if ((uint)slice.Length < 3 ||
!slice.Slice(1).StartsWith("bc", StringComparison.OrdinalIgnoreCase)) // Match the string "bc" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 3;
slice = inputSpan.Slice(pos);
break;
case 'D' or 'd':
if ((uint)slice.Length < 3 ||
!slice.Slice(1).StartsWith("ef", StringComparison.OrdinalIgnoreCase)) // Match the string "ef" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 3;
slice = inputSpan.Slice(pos);
break;
default:
return false; // The input didn't match.
}
}
// The input matched.
base.runtextpos = pos;
base.Capture(0, matchStart, pos);
return true;
}
Cílem zdrojového vygenerovaného kódu je pochopitelný, s snadno sledovatelnou strukturou, s komentáři vysvětlující, co se provádí v jednotlivých krocích, a obecně s kódem vygenerovaným v rámci hlavního principu, že generátor by měl generovat kód, jako by ho napsal člověk. I když je zapojeno zpětné vyhledávání, stane se jeho struktura součástí struktury kódu, místo toho, aby se spoléhali na zásobník k označení, kam se má přejít dál. Například, tady je kód pro stejnou vygenerovanou funkci odpovídající, když je výraz [ab]*[bc].
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
int pos = base.runtextpos;
int matchStart = pos;
int charloop_starting_pos = 0, charloop_ending_pos = 0;
ReadOnlySpan<char> slice = inputSpan.Slice(pos);
// Match a character in the set [ABab] greedily any number of times.
//{
charloop_starting_pos = pos;
int iteration = slice.IndexOfAnyExcept(Utilities.s_ascii_600000006000000);
if (iteration < 0)
{
iteration = slice.Length;
}
slice = slice.Slice(iteration);
pos += iteration;
charloop_ending_pos = pos;
goto CharLoopEnd;
CharLoopBacktrack:
if (Utilities.s_hasTimeout)
{
base.CheckTimeout();
}
if (charloop_starting_pos >= charloop_ending_pos ||
(charloop_ending_pos = inputSpan.Slice(charloop_starting_pos, charloop_ending_pos - charloop_starting_pos).LastIndexOfAny(Utilities.s_ascii_C0000000C000000)) < 0)
{
return false; // The input didn't match.
}
charloop_ending_pos += charloop_starting_pos;
pos = charloop_ending_pos;
slice = inputSpan.Slice(pos);
CharLoopEnd:
//}
// Advance the next matching position.
if (base.runtextpos < pos)
{
base.runtextpos = pos;
}
// Match a character in the set [BCbc].
if (slice.IsEmpty || ((uint)((slice[0] | 0x20) - 'b') > (uint)('c' - 'b')))
{
goto CharLoopBacktrack;
}
// The input matched.
pos++;
base.runtextpos = pos;
base.Capture(0, matchStart, pos);
return true;
}
Můžete vidět strukturu backtrackingu v kódu se značkou CharLoopBacktrack, která se vygeneruje pro místo, kam se vracet, a goto se používá k přeskočení na toto místo, když další část regulárního výrazu selže.
Pokud se podíváte na implementaci RegexCompiler kódu a generátor zdrojového kódu, budou vypadat velmi podobně: podobně pojmenované metody, podobná struktura volání a dokonce podobné komentáře v celé implementaci. Ve většině případů mají za následek stejný kód, i když jeden v IL a druhý v jazyce C#. Kompilátor jazyka C# je samozřejmě zodpovědný za překlad jazyka C# do IL, takže výsledný il v obou případech pravděpodobně nebude identický. Zdrojový generátor spoléhá na to v různých případech, přičemž využívá skutečnost, že kompilátor jazyka C# dále optimalizuje různé konstrukce jazyka C#. Existuje několik konkrétních věcí, které zdrojový generátor vytvoří optimalizovanější odpovídající kód, než dělá RegexCompiler. Například v jednom z předchozích příkladů můžete zobrazit zdrojový generátor vygenerující příkaz switch s jednou větví pro 'a' a jinou větví pro 'b'. Vzhledem k tomu, že kompilátor jazyka C# je velmi dobrý při optimalizaci výrazů switch, má k dispozici různé strategie pro efektivní optimalizaci, zatímco generátor zdrojů má speciální optimalizaci, kterou RegexCompiler nemá. V případě alternací zdrojový generátor analyzuje všechny větve a pokud může prokázat, že každá větev začíná jiným počátečním znakem, vytvoří příkaz switch přes tento první znak a vyhne se výstupu jakéhokoli kódu zpětného navracení této alternace.
Tady je trochu složitější příklad. Alternace jsou důkladněji analyzovány, aby se zjistilo, jestli je možné je refaktorovat způsobem, který by usnadnil jejich optimalizaci pomocí backtrackingových enginů a vedl k jednodušší zdrojové generaci kódu. Jedna z těchto optimalizací podporuje extrahování běžných předpon z větví a pokud je alternace atomická tak, aby řazení nezáleželo, přeuspořádejte větve tak, aby umožňovaly další takovou extrakci. Můžete vidět dopad tohoto vzorce Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sundaypro následující pracovní den , který vytváří odpovídající funkci takto:
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
int pos = base.runtextpos;
int matchStart = pos;
char ch;
ReadOnlySpan<char> slice = inputSpan.Slice(pos);
// Match with 6 alternative expressions, atomically.
{
int alternation_starting_pos = pos;
// Branch 0
{
if ((uint)slice.Length < 6 ||
!slice.StartsWith("monday", StringComparison.OrdinalIgnoreCase)) // Match the string "monday" (ordinal case-insensitive)
{
goto AlternationBranch;
}
pos += 6;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 1
{
if ((uint)slice.Length < 7 ||
!slice.StartsWith("tuesday", StringComparison.OrdinalIgnoreCase)) // Match the string "tuesday" (ordinal case-insensitive)
{
goto AlternationBranch1;
}
pos += 7;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch1:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 2
{
if ((uint)slice.Length < 9 ||
!slice.StartsWith("wednesday", StringComparison.OrdinalIgnoreCase)) // Match the string "wednesday" (ordinal case-insensitive)
{
goto AlternationBranch2;
}
pos += 9;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch2:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 3
{
if ((uint)slice.Length < 8 ||
!slice.StartsWith("thursday", StringComparison.OrdinalIgnoreCase)) // Match the string "thursday" (ordinal case-insensitive)
{
goto AlternationBranch3;
}
pos += 8;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch3:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 4
{
if ((uint)slice.Length < 6 ||
!slice.StartsWith("fr", StringComparison.OrdinalIgnoreCase) || // Match the string "fr" (ordinal case-insensitive)
((((ch = slice[2]) | 0x20) != 'i') & (ch != 'İ')) || // Match a character in the set [Ii\u0130].
!slice.Slice(3).StartsWith("day", StringComparison.OrdinalIgnoreCase)) // Match the string "day" (ordinal case-insensitive)
{
goto AlternationBranch4;
}
pos += 6;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch4:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 5
{
// Match a character in the set [Ss].
if (slice.IsEmpty || ((slice[0] | 0x20) != 's'))
{
return false; // The input didn't match.
}
// Match with 2 alternative expressions, atomically.
{
if ((uint)slice.Length < 2)
{
return false; // The input didn't match.
}
switch (slice[1])
{
case 'A' or 'a':
if ((uint)slice.Length < 8 ||
!slice.Slice(2).StartsWith("turday", StringComparison.OrdinalIgnoreCase)) // Match the string "turday" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 8;
slice = inputSpan.Slice(pos);
break;
case 'U' or 'u':
if ((uint)slice.Length < 6 ||
!slice.Slice(2).StartsWith("nday", StringComparison.OrdinalIgnoreCase)) // Match the string "nday" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 6;
slice = inputSpan.Slice(pos);
break;
default:
return false; // The input didn't match.
}
}
}
AlternationMatch:;
}
// The input matched.
base.runtextpos = pos;
base.Capture(0, matchStart, pos);
return true;
}
Zároveň se zdrojový generátor musí potýkat s dalšími problémy, které při přímém výstupu do IL zkrátka neexistují. Pokud se podíváte na několik předchozích příkladů kódu, uvidíte složené závorky, které jsou neobvykle zakomentované. To není chyba. Generátor zdrojového kódu rozpoznává, že pokud tyto složené závorky nebyly okomentovány, struktura zpětného sledování spoléhá na přeskakování zvenčí oboru na popisek definovaný uvnitř tohoto oboru; takový popisek by nebyl viditelný pro goto a kód by se nepodařilo zkompilovat. Generátor zdrojového kódu se proto musí vyhnout tomu, aby mu v cestě stál nějaký rozsah. V některých případech jednoduše zakomentuje rozsah, jak jsme tady udělali. V jiných případech, kdy to není možné, se někdy může vyhnout konstrukcím, které vyžadují obory (například blok s více příkazy if), pokud by to mohlo být problematické.
Zdrojový generátor zpracovává všechno, co zpracovává RegexCompiler, s jednou výjimkou. Stejně jako při zpracování RegexOptions.IgnoreCase nyní implementace používají tabulku velikosti písma k vytvoření sad během konstrukce a jak zpětné porovnávání IgnoreCase potřebuje konzultovat tuto tabulku. Tato tabulka je interní pro System.Text.RegularExpressions.dll a alespoň prozatím k ní nemá přístup kód externího sestavení, včetně kódu generovaného generátorem zdroje. Díky tomu je zpracování IgnoreCase zpětných odkazů problémem ve zdrojovém generátoru a nejsou podporovány. Jedná se o jedinou strukturu, kterou zdrojový generátor nepodporuje, ale RegexCompiler ano. Pokud se pokusíte použít vzor, který má jednu z těchto (což je vzácné), generátor zdroje nevygeneruje vlastní implementaci a místo toho se vrátí do mezipaměti pravidelné Regex instance:
Také ani RegexCompiler ani generátor zdroje nepodporují nový RegexOptions.NonBacktracking. Pokud zadáte RegexOptions.Compiled | RegexOptions.NonBacktracking, příznak Compiled bude ignorován, a pokud zadáte NonBacktracking pro zdrojový generátor, bude se podobně vracet k ukládání běžné instance Regex do mezipaměti.
Kdy ji použít
Obecné pokyny jsou, pokud můžete použít generátor zdrojů, použijte ho. Pokud dnes v současnosti používáte Regex v jazyce C# s argumenty známými v době kompilace, a to zejména v případě, že již používáte RegexOptions.Compiled (protože regulární výraz byl identifikován jako kritické místo, které by mělo prospěch z rychlejší propustnosti), měli byste raději použít generátor kódu. Generátor zdrojového kódu poskytne vašemu regulárnímu výrazu následující výhody:
- Všechny výhody propustnosti
RegexOptions.Compiled. - Výhody startupu, který nemusí provádět veškerou analýzu, zpracování a kompilaci regulárních výrazů během běhu programu.
- Možnost použití kompilace před spuštěním s kódem vygenerovaným pro regulární výraz.
- Lepší ladění a porozumění regulárnímu výrazu.
- Možnost pro zmenšení velikosti oříznuté aplikace odstraněním velkých částí kódu spojeného s
RegexCompiler(a potenciálně i samotné generování reflexe).
Při použití s možností, jako je například RegexOptions.NonBacktracking, pro kterou zdrojový generátor nemůže vygenerovat vlastní implementaci, bude stále generovat kód pro ukládání do mezipaměti a XML komentáře popisující implementaci, což z něj činí cenný nástroj. Hlavní nevýhodou generátoru kódu ze zdrojového textu je, že do sestavení vkládá dodatečný kód, což může vést ke zvýšení velikosti. Čím více regulárních výrazů máte ve vaší aplikaci, a čím větší jsou, tím více kódu se pro ně vygeneruje. V některých situacích, stejně jako RegexOptions.Compiled může být zbytečný, tak i generátor zdrojového kódu může být zbytečný. Pokud máte například regulární výraz, který je potřeba jen zřídka a pro který výkon není důležitý, může být vhodnější pouze spoléhat na interpreta pro takové sporadické použití.
Důležité
.NET 7 obsahuje analyzátor , který identifikuje použití Regex , které by bylo možné převést na generátor zdroje, a opravovač, který provede převod za vás: