Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of mappen te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen om mappen te wijzigen.
Een reguliere expressie, of regex, is een tekenreeks waarmee een ontwikkelaar een patroon kan uitdrukken waarnaar wordt gezocht, waardoor het een veelgebruikte manier is om tekst te zoeken en resultaten te extraheren als een subset uit de gezochte tekenreeks. In .NET wordt de System.Text.RegularExpressions naamruimte gebruikt om Regex instanties en statische methoden te definiëren, en te overeenkomen met door de gebruiker gedefinieerde patronen. In dit artikel leert u hoe u brongeneratie kunt gebruiken om exemplaren te genereren Regex om de prestaties te optimaliseren.
Notitie
Gebruik waar mogelijk reguliere expressies die door de bron zijn gegenereerd in plaats van reguliere expressies te compileren met behulp van de RegexOptions.Compiled optie. Het genereren van bronnen kan ervoor zorgen dat uw app sneller kan worden gestart, sneller kan worden uitgevoerd en beter kan worden ingekort. Als u wilt weten wanneer brongeneratie mogelijk is, raadpleegt u Wanneer gebruiken.
Gecompileerde reguliere expressies
Wanneer u schrijft new Regex("somepattern"), gebeuren er een paar dingen. Het opgegeven patroon wordt geparseerd, zowel om de geldigheid van het patroon te garanderen als om het te transformeren in een interne structuur die de geparseerde regex vertegenwoordigt. De structuur wordt vervolgens op verschillende manieren geoptimaliseerd, waardoor het patroon wordt omgezet in een functioneel equivalente variatie die efficiënter kan worden uitgevoerd. De structuur wordt geschreven in een vorm die kan worden geïnterpreteerd als een reeks opcodes en operanden die instructies bieden aan de regex-interpreterengine over hoe te matchen. Wanneer een overeenkomst wordt uitgevoerd, doorloopt de interpreter deze instructies, zodat deze worden verwerkt op basis van de invoertekst. Bij het instantiëren van een nieuw Regex exemplaar of het aanroepen van een van de statische methoden Regexop, is de interpreter de standaardengine die wordt gebruikt.
Wanneer u RegexOptions.Compiled specificeert, wordt al het werk dat tijdens de constructieperiode wordt uitgevoerd alsnog uitgevoerd. De resulterende instructies worden verder getransformeerd door de compiler op basis van reflectie-emit in IL-instructies die naar een paar DynamicMethod objecten worden geschreven. Deze DynamicMethod methoden worden aangeroepen wanneer een match wordt uitgevoerd. Deze IL doet in wezen precies wat de interpreter zou doen, behalve gespecialiseerd voor het exacte patroon dat wordt verwerkt. Als het patroon bijvoorbeeld [ac] bevat, ziet de interpreter een opcode met de tekst 'vergelijk het invoerteken op de huidige positie met de set die in deze beschrijvingsset is gespecificeerd'. Wanneer de gecompileerde IL code zou bevatten die effectief zegt: "vergelijk het invoerkarakter op de huidige positie met 'a' of 'c'". Deze speciale behuizing en de mogelijkheid om optimalisaties uit te voeren op basis van kennis van het patroon zijn enkele van de belangrijkste redenen waarom het opgeven RegexOptions.Compiled van een veel snellere overeenkomende doorvoer oplevert dan de interpreter.
Er zijn verschillende nadelen aan RegexOptions.Compiled. Het meest impactvol is dat het kostbaar is om te bouwen. Niet alleen worden alle dezelfde kosten betaald als voor de interpreter, maar vervolgens moeten de resulterende RegexNode structuur en de gegenereerde opcodes/operanden in IL worden gecompileerd, wat niet-triviale kosten met zich meebrengt. De gegenereerde IL moet verder worden gecompileerd op basis van JIT tijdens het eerste gebruik, wat leidt tot nog meer kosten bij het opstarten.
RegexOptions.Compiled vertegenwoordigt een fundamentele afweging tussen overhead voor het eerste gebruik en de overhead voor elk volgend gebruik. Het gebruik van System.Reflection.Emit remt ook het gebruik van RegexOptions.Compiled in bepaalde omgevingen; sommige besturingssystemen staan niet toe dat dynamisch gegenereerde code wordt uitgevoerd en op dergelijke systemen wordt Compiled een no-op.
Broncodegeneratie
.NET 7 heeft een nieuwe RegexGenerator brongenerator geïntroduceerd. Een brongenerator is een onderdeel dat wordt aangesloten op de compiler en de compilatie-eenheid verbetert met aanvullende broncode. De .NET SDK bevat een brongenerator die het GeneratedRegexAttribute kenmerk herkent op een gedeeltelijke methode die retourneert Regex. Vanaf .NET 9 kan het kenmerk ook worden toegepast op gedeeltelijke eigenschappen. De broncodegenerator biedt een implementatie van die methode of eigenschap die alle logica voor de Regex bevat. U hebt bijvoorbeeld eerder code als volgt geschreven:
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
}
}
Als u de brongenerator wilt gebruiken, herschrijft u de vorige code als volgt:
[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
}
}
Vanaf .NET 9 kunt u de GeneratedRegexAttribute eigenschap ook toepassen op een gedeeltelijke eigenschap in plaats van op een gedeeltelijke methode. Dit wordt ingeschakeld door de ondersteuning van C# 13 voor gedeeltelijke eigenschappen. In het volgende voorbeeld ziet u het equivalent van de eigenschap:
[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
}
}
Aanbeveling
De RegexOptions.Compiled vlag wordt genegeerd door de brongenerator, dus deze is niet nodig in de door de bron gegenereerde versie.
De gegenereerde implementatie van AbcOrDefGeneratedRegex() slaat op vergelijkbare wijze een singleton-exemplaar van Regex op, dus er is geen extra caching nodig om de code te gebruiken.
De volgende afbeelding is een schermopname van het door de bron gegenereerde cache-exemplaar internal naar de Regex subklasse die de brongenerator uitgeeft:
Maar zoals te zien is, doet het niet alleen new Regex(...). In plaats daarvan verzendt de brongenerator als C#-code een aangepaste Regex- afgeleide implementatie met logica die vergelijkbaar is met wat RegexOptions.Compiled in IL wordt verzonden. U krijgt alle doorvoerprestaties van RegexOptions.Compiled (meer zelfs), en de opstartvoordelen van Regex.CompileToAssembly, maar zonder de complexiteit van CompileToAssembly. De bron die wordt verzonden, maakt deel uit van uw project, wat betekent dat het ook eenvoudig kan worden weergegeven en foutopsporing kan worden uitgevoerd.
Aanbeveling
Klik in Visual Studio met de rechtermuisknop op uw gedeeltelijke methode of eigenschapsdeclaratie en selecteer Ga naar definitie. Of selecteer het projectknooppunt in Solution Explorer en vouw vervolgens Dependencies>Analyzers>System.Text.RegularExpressions.Generator>System.Text.RegularExpressions.Generator.RegexGenerator>RegexGenerator.g.cs uit om de gegenereerde C#-code van deze regex-generator te zien.
U kunt onderbrekingspunten instellen, u kunt er stapsgewijs doorheen en u kunt het gebruiken als leerhulpmiddel om precies te begrijpen hoe de regex-engine uw patroon verwerkt met uw invoer. De generator genereert zelfs xml-opmerkingen (triple-slash) om de expressie begrijpelijk te maken en waar deze wordt gebruikt.
Binnen de door de bron gegenereerde bestanden
Met .NET 7 werden zowel de brongenerator als RegexCompiler bijna volledig herschreven, waarbij de structuur van de gegenereerde code fundamenteel werd gewijzigd. Deze benadering is uitgebreid om alle constructies te verwerken (met één kanttekening), en zowel RegexCompiler als de brongenerator komen volgens de nieuwe benadering nog steeds meestal 1:1 overeen met elkaar. Bekijk de uitvoer van de brongenerator voor een van de primaire functies uit de abc|def expressie:
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;
}
Het doel van de door de broncode gegenereerde code is begrijpelijk te zijn, met een eenvoudig te volgen structuur, met opmerkingen waarin wordt uitgelegd wat er in elke stap wordt gedaan, en in het algemeen met code die wordt verzonden volgens het leidende principe dat de generator code moet verzenden alsof een mens deze had geschreven. Zelfs wanneer er backtracking bij komt kijken, wordt de structuur van de backtracking onderdeel van de structuur van de code, in plaats van te vertrouwen op een stack om aan te geven waar de volgende sprong naartoe moet gaan. Hier ziet u bijvoorbeeld de code voor dezelfde gegenereerde overeenkomende functie als de expressie:[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;
}
U kunt de structuur van de backtracking in de code zien, met een CharLoopBacktrack label dat wordt uitgegeven om terug te gaan naar en een goto gebruikt om naar die locatie te springen wanneer een volgend gedeelte van de regex mislukt.
Als u naar de code kijkt die RegexCompiler implementeert en naar de brongenerator, zullen ze er zeer vergelijkbaar uitzien: methoden met vergelijkbare namen, vergelijkbare aanroepstructuur en zelfs vergelijkbare opmerkingen gedurende de implementatie. Voor het grootste deel resulteren ze in identieke code, maar wel één in IL en één in C#. Natuurlijk is de C#-compiler vervolgens verantwoordelijk voor het vertalen van de C# in IL, dus de resulterende IL in beide gevallen is waarschijnlijk niet identiek. De brongenerator is afhankelijk van dat in verschillende gevallen, waarbij gebruik wordt gemaakt van het feit dat de C#-compiler verschillende C#-constructies verder zal optimaliseren. Er zijn enkele specifieke dingen die de brongenerator dus meer geoptimaliseerde overeenkomende code produceert dan wel RegexCompiler. In een van de vorige voorbeelden ziet u bijvoorbeeld dat de brongenerator een switchinstructie verzendt, met één vertakking voor 'a' en een andere vertakking voor 'b'. Omdat de C#-compiler zeer goed is bij het optimaliseren van switch-instructies, met meerdere strategieën die beschikbaar zijn om dit efficiënt te doen, heeft de brongenerator een speciale optimalisatie die RegexCompiler dat niet doet. Voor alternaties kijkt de brongenerator naar alle vertakkingen en als het kan bewijzen dat elke vertakking begint met een ander beginteken, wordt er een switch-instructie over dat eerste teken verzonden en wordt voorkomen dat er backtrackingcode voor die alternatie wordt uitgevoerd.
Hier volgt een iets gecompliceerder voorbeeld hiervan. Alternatieven worden grondiger geanalyseerd om te bepalen of het mogelijk is om ze te herstructureren op een manier zodat ze gemakkelijker geoptimaliseerd kunnen worden door de backtracking-engines en wat zal leiden tot simpeler gegenereerde broncode. Een dergelijke optimalisatie ondersteunt het extraheren van veelvoorkomende voorvoegsels uit vertakkingen en als de alternatie atomisch is, zodat volgorde niet uitmaakt, kunt u vertakkingen opnieuw ordenen om meer dergelijke extractie mogelijk te maken. U kunt de impact hiervan voor het volgende weekdagpatroon Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sundayzien, waardoor een overeenkomende functie als volgt wordt geproduceerd:
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;
}
Tegelijkertijd heeft de brongenerator andere problemen die aangepakt moeten worden die eenvoudigweg niet bestaan bij directe uitvoer naar IL. Als u een paar codevoorbeelden verder terugkijkt, ziet u enkele accolades die op een enigszins ongewone manier zijn gecommentarieerd. Dat is geen vergissing. De brongenerator erkent dat, als deze accolades niet uitgecommentarieerd zijn, de structuur van de backtracking afhankelijk is van springen van buiten de scope naar een label dat binnen die scope is gedefinieerd; een dergelijk label zou niet zichtbaar zijn voor een goto en de code zou niet compileren. De brongenerator moet dus voorkomen dat er een toepassingsgebied in de weg zit. In sommige gevallen zal het gewoon het bereik uitcommentariëren zoals hier is gedaan. In andere gevallen waarin dat niet mogelijk is, kan het zijn dat constructies waarvoor scopes nodig zijn (zoals een blok met meerdere if-instructies) worden vermeden als dit problematisch zou blijken.
De brongenerator verwerkt alles wat RegexCompiler behandelt, met één uitzondering. Net als bij de verwerking RegexOptions.IgnoreCase gebruiken de implementaties nu een hoofdlettertabel om sets tijdens de constructie te genereren, en IgnoreCase moeten bij backreference-matchen deze hoofdlettertabel raadplegen. Deze tabel is intern voor System.Text.RegularExpressions.dll, en voorlopig heeft de code buiten die assembly (inclusief code die wordt verzonden door de brongenerator) geen toegang. Dit maakt het verwerken IgnoreCase van backreferences een uitdaging in de brongenerator en ze worden niet ondersteund. Dit is de enige constructie die niet wordt ondersteund door de brongenerator die wordt ondersteund door RegexCompiler. Als u probeert een patroon te gebruiken dat een van deze (zeldzaam) heeft, zal de brongenerator geen aangepaste implementatie verzenden en in plaats daarvan terugvallen op het opslaan van een normaal Regex exemplaar:
Ook ondersteunt noch RegexCompiler noch de brongenerator de nieuwe RegexOptions.NonBacktracking. Als u RegexOptions.Compiled | RegexOptions.NonBacktracking opgeeft, wordt de Compiled vlag gewoon genegeerd, en als u NonBacktracking aan de brongenerator opgeeft, zal deze op dezelfde manier terugvallen op het cachen van een reguliere Regex instantie.
Wanneer te gebruiken
De algemene richtlijn is: als u de brongenerator kunt gebruiken, gebruik die dan. Als u Regex vandaag in C# gebruikt met argumenten die bekend zijn tijdens het compileren, en vooral als u al gebruikt RegexOptions.Compiled (omdat de regex is geïdentificeerd als een hot spot die zou profiteren van snellere doorvoer), moet u de brongenerator liever gebruiken. De brongenerator biedt uw regex de volgende voordelen:
- Alle doorvoervoordelen van
RegexOptions.Compiled. - De opstartvoordelen van het niet hoeven uitvoeren van alle regex parsering, analyse en compilatie tijdens runtime.
- De optie om vooraf compilatie te gebruiken met de code die voor de regex is gegenereerd.
- Betere foutopsporing en inzicht in de regex.
- De mogelijkheid om de grootte van uw gesnoeide app te verkleinen door grote delen code uit te snijden die gekoppeld zijn aan
RegexCompiler(en mogelijk zelfs reflectie-uitzending zelf).
Wanneer deze wordt gebruikt met een optie zoals RegexOptions.NonBacktracking waarvoor de brongenerator geen aangepaste implementatie kan genereren, worden er nog steeds caching- en XML-opmerkingen verzonden waarin de implementatie wordt beschreven, waardoor deze waardevol is. Het belangrijkste nadeel van de brongenerator is dat er extra code aan uw assembly wordt toegevoegd, waardoor er potentieel is voor een grotere omvang. Hoe meer regexes in uw app en hoe groter ze zijn, hoe meer code er voor wordt verzonden. In sommige situaties, net zoals RegexOptions.Compiled mogelijk onnodig is, kan de brongenerator dat ook zijn. Als u bijvoorbeeld een regex hebt die slechts zelden nodig is en waarvoor doorvoer niet van belang is, kan het nuttiger zijn om alleen te vertrouwen op de interpreter voor dat sporadische gebruik.
Belangrijk
.NET 7 bevat een analyzer die het gebruik identificeert van Regex dat kan worden geconverteerd naar de brongenerator, en een reparatietool die de conversie voor u uitvoert.