Generátory zdrojů regulárních výrazů .NET
Regulární výraz neboli regulární výraz je řetězec, který vývojářům umožňuje vyjádřit hledaný vzor, což představuje běžný způsob, jak hledat text a extrahovat výsledky jako podmnožinu z 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 vaší aplikaci pomoct rychleji, rychleji spustit a lépe se oříznout. 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 formuláře, který lze interpretovat jako řadu opcode a operandů, které poskytují pokyny interpretačnímu stroji regulárních výrazů, jak se shodovat. Když se provede shoda, interpret je jednoduše provede těmito pokyny a zpracuje je proti vstupnímu textu. Při vytváření instance nové Regex
instance nebo volání jedné ze statických metod Regex
je interpret použit výchozí modul.
Při zadání RegexOptions.Compiledse provede všechna stejná stavební práce. 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 interpret udělal, s výjimkou specializovaného na přesný vzor, který se zpracovává. Pokud například vzor obsahuje [ac]
, interpret by viděl opcode, který říká "shoda vstupního znaku na aktuální pozici proti sadě zadané v tomto popisu sady". Zatímco zkompilovaná il by obsahovala kód, který efektivně říká, "odpovídá vstupnímu znaku na aktuální pozici proti '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 stejné náklady zaplacené za interpreta, ale pak musí zkompilovat výsledný RegexNode
strom a vygenerované opcodes/operandy do IL, což přidává nevýkonné výdaje. 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í zdroje
.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 (verze 7 a novější) obsahuje generátor zdrojů, který rozpozná GeneratedRegexAttribute atribut částečné metody, která vrací Regex
. Generátor zdroje poskytuje implementaci této metody, která obsahuje veškerou logiku Regex
pro . 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
}
}
Tip
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 zdrojové instance internal
vygenerované v mezipaměti do Regex
podtřídy, kterou generuje generátor zdrojů:
Ale jak je vidět, není to jen dělá new Regex(...)
. Zdrojový generátor spíše generuje jako kód jazyka C# vlastní Regex
implementaci odvozenou odvozující logiku podobnou tomu, co RegexOptions.Compiled
generuje v IL. Získáte všechny výhody RegexOptions.Compiled
výkonu propustnosti (ve skutečnosti) a výhody Regex.CompileToAssembly
spuštění , ale bez složitosti CompileToAssembly
. Zdroj, který se vygeneruje, je součástí projektu, což znamená, že je také snadno zobrazitelný a laditelný.
Tip
V sadě Visual Studio klikněte pravým tlačítkem na deklaraci částečné metody a vyberte Přejít k definici. Nebo případně vyberte uzel projektu v Průzkumník řešení a pak rozbalte závislosti>Analyzers>System.Text.RegularExpressions.Generator>System.Text.RegularExpressions.Generator.RegexGenerator>RegexGenerator.g.cs zobrazit vygenerovaný kód C# 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 trojité lomítko (XML), aby byl výraz srozumitelný na první pohled a tam, kde se používá.
Uvnitř zdrojových souborů
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 RegexCompiler
zdrojový generátor se stále mapuje většinou 1:1 s ostatními, a to po novém 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 zpětné navracení zapojeno, struktura zpětného navracení se stane součástí struktury kódu, místo aby se spoléhala na zásobník, aby bylo zřejmé, kam se má přejít. Tady je například kód pro stejnou vygenerovanou odpovídající funkci, když je [ab]*[bc]
výraz:
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 zpětného navracení v kódu s CharLoopBacktrack
popiskem, který se vygeneruje pro umístění zpětného navracení a goto
použitý 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 příkazů přepínače, s několika strategiemi, které jsou k dispozici pro efektivní způsob, má generátor zdrojů speciální optimalizaci, která RegexCompiler
ne. V případě alternací se zdrojový generátor podívá na všechny větve a pokud může prokázat, že každá větev začíná jiným počátečním znakem, vygeneruje příkaz switch přes tento první znak a vyhne se výstupu jakéhokoli kódu zpětného navracení tohoto alternace.
Tady je trochu složitější příklad. Alternace jsou silně analyzovány, aby se zjistilo, jestli je možné refaktorovat způsobem, který je snadněji optimalizuje moduly pro navracení a které vedou k jednoduššímu zdrojovému 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|Sunday
pro 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;
}
Ve stejnou dobu má zdrojový generátor další problémy, které se musí potýkat s tím, že při přímém výstupu do IL prostě neexistují. Pokud se podíváte na několik příkladů kódu zpět, můžete vidět některé složené závorky poněkud podivně okomentované. To není chyba. Generátor zdrojů rozpozná, že pokud tyto složené závorky nebyly okomentovány, struktura zpětného navracení se spoléhá na přeskakování mimo obor na popisek definovaný uvnitř tohoto oboru; takový popisek by takový popisek nebyl viditelný goto
a kód by se nepodařilo zkompilovat. Zdrojový generátor se proto musí vyhnout rozsahu v cestě. 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 konstruktorům, které vyžadují obory (například blok s více příkazy if
), pokud by to bylo problematické.
Zdrojový generátor zpracovává všechno RegexCompiler
, s jednou výjimkou. Stejně jako v případě zpracování RegexOptions.IgnoreCase
teď implementace používají k vygenerování sad v době výstavby tabulku s velikostí a způsob, jakým IgnoreCase
porovnávání zpětného odvozování potřebuje prohlédnout tabulku s velikostí a velikostí. Tato tabulka je interní System.Text.RegularExpressions.dll
pro toto sestavení (včetně kódu generovaného generátorem zdroje) a prozatím k němu nemá přístup aspoň kód, který je externí pro toto sestavení (včetně kódu generovaného generátorem zdroje). Díky tomu zpracování IgnoreCase
backreference představuje problém ve zdrojovém generátoru a nejsou podporované. Jedná se o jeden konstruktor, který není podporován zdrojovým generátorem RegexCompiler
, který je podporován . 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
generátor zdroje nepodporuje nový RegexOptions.NonBacktracking
. Pokud zadáte RegexOptions.Compiled | RegexOptions.NonBacktracking
, Compiled
příznak se bude ignorovat a pokud zadáte NonBacktracking
do zdrojového generátoru, bude se podobně vracet do mezipaměti běžné Regex
instance.
Kdy ji použít
Obecné pokyny jsou, pokud můžete použít generátor zdrojů, použijte ho. Pokud používáte Regex
dnes v jazyce C# s argumenty známými v době kompilace, a to zejména v případě, že už používáte RegexOptions.Compiled
(protože regulární výraz byl identifikován jako aktivní bod, který by měl prospěch z rychlejší propustnosti), měli byste raději použít generátor zdrojů. Generátor zdrojů poskytne regulárnímu výrazu následující výhody:
- Všechny výhody propustnosti .
RegexOptions.Compiled
- Výhody spuštění, které nemusí provádět všechny analýzy, analýzy a kompilace regulárních výrazů za běhu.
- Možnost použít předem připravenou kompilaci s kódem vygenerovaným pro regulární výraz.
- Lepší ladění a porozumění regulárnímu výrazu.
- Možnost zmenšit velikost oříznuté aplikace oříznutím velkého množství kódu spojeného s
RegexCompiler
kódem (a potenciálně i reflexe se vygeneruje).
Při použití s možností, jako RegexOptions.NonBacktracking
je například zdroj generátor nemůže vygenerovat vlastní implementaci, bude stále generovat ukládání do mezipaměti a komentáře XML popisující implementaci, což z něj dělá cenné. Hlavní nevýhodou zdrojového generátoru je, že do sestavení generuje další kód, takže existuje potenciál pro zvýšení velikosti. Čím více regulárních výrazů 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
by to mohlo být zbytečné, takže také může být zdrojem generátoru. Pokud máte například regulární výraz, který je potřeba jen zřídka a pro kterou propustnost nezáleží, může být vhodnější jen spoléhat na interpreta pro toto 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: