Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Ein regulärer Ausdruck (RegEx) ist eine Zeichenfolge, die es einem Entwickler ermöglicht, ein gesuchtes Muster auszudrücken. Daher werden reguläre Ausdrücke häufig verwendet, um Text zu durchsuchen und Ergebnisse als Teilmenge aus der gesuchten Zeichenfolge zu extrahieren. In .NET wird der Namespace System.Text.RegularExpressions verwendet, um Regex-Instanzen und statische Methoden zu definieren und mit benutzerdefinierten Mustern abzugleichen. In diesem Artikel erfahren Sie, wie Sie mithilfe der Quellgenerierung Regex-Instanzen generieren, um die Leistung zu optimieren.
Hinweis
Verwenden Sie nach Möglichkeit die quellgenerierten regulären Ausdrücke, anstatt reguläre Ausdrücke mithilfe der RegexOptions.Compiled-Option zu kompilieren. Die Quellgenerierung kann dazu beitragen, dass Ihre App schneller gestartet wird, schneller ausgeführt wird und besser gekürzt werden kann. Informationen dazu, wann die Quellgenerierung möglich ist, finden Sie unter Verwendung.
Kompilierte reguläre Ausdrücke
Wenn Sie new Regex("somepattern") schreiben, passieren mehrere Dinge: Das angegebene Muster wird analysiert, um seine Gültigkeit sicherzustellen und es in einen internen Baum zu verwandeln, der den analysierten RegEx darstellt. Der Baum wird dann auf verschiedene Arten optimiert, wobei das Muster in eine funktional äquivalente Variante umgewandelt wird, die effizienter ausgeführt werden kann. Der Baum wird in ein Format geschrieben, das als eine Reihe von Opcodes und Operanden interpretiert werden kann und dem Regex-Interpreter Anweisungen gibt, wie der Abgleich durchgeführt wird. Wenn ein Abgleich durchgeführt wird, werden diese Anweisungen einfach vom Interpreter durchlaufen und im Kontext des Eingabetextes verarbeitet. Wenn Sie eine neue Regex-Instanz instanziieren oder eine der statischen Methoden für Regexaufrufen, wird standardmäßig die Interpreter-Engine verwendet.
Wenn Sie RegexOptions.Compiled angeben, werden die gleichen Schritte zur Erstellungszeit ausgeführt. Die resultierenden Anweisungen werden vom reflektionsemittierenden Compiler in IL-Anweisungen umgewandelt, die dann in einige DynamicMethod-Objekte geschrieben werden. Wenn eine Übereinstimmung durchgeführt wird, werden diese DynamicMethod Methoden aufgerufen. Diese IL macht im Wesentlichen genau das, was der Interpreter machen würde, jedoch spezialisiert auf das exakte Muster, das verarbeitet wird. Wenn das Muster z. B. [ac] enthält, würde der Interpreter einen Opcode sehen, der besagt: „Entspricht das Eingabezeichen an der aktuellen Position der in dieser Mengenbeschreibung angegebenen Menge“. Während die kompilierte Zwischensprache Code enthalten würde, der effektiv besagt: „das Eingabezeichen an der aktuellen Position mit 'a' oder 'c' abgleichen“. Dieser Spezialfall und die Möglichkeit, Optimierungen basierend auf der Kenntnis des Musters durchzuführen, sind einige der Hauptgründe, warum die Angabe von RegexOptions.Compiled im Vergleich zum Interpreter zu einem viel schnelleren Matching-Durchsatz führt.
RegexOptions.Compiled hat mehrere Nachteile. Am meisten Auswirkungen hat, dass die Konstruktion kostspielig ist. Nicht nur fallen beim Interpreter die gleichen Kosten an, sondern es muss auch die resultierende RegexNode-Struktur und die generierten Opcodes/Operanden in die Zwischensprache (IL) kompiliert werden, was einen nicht unerheblichen Mehraufwand bedeutet. Die generierte Zwischensprache muss außerdem bei der ersten Verwendung JIT-kompiliert werden, was den Aufwand beim Start noch weiter erhöht.
RegexOptions.Compiled stellt einen grundlegenden Kompromiss zwischen dem Aufwand bei der ersten Verwendung und dem Aufwand bei jeder nachfolgenden Verwendung dar. Die Verwendung von System.Reflection.Emit hemmt auch die Verwendung von RegexOptions.Compiled in bestimmten Umgebungen. Einige Betriebssysteme lassen die Ausführung von dynamisch generiertem Code nicht zu, und auf solchen Systemen kann Compiled nicht verwendet werden.
Quellengenerierung
.NET 7 hat einen neuen RegexGenerator-Quellgenerator eingeführt. Ein Quellgenerator ist eine Komponente, die an den Compiler ansteckt und die Kompilierungseinheit mit zusätzlichem Quellcode erweitert. Das .NET SDK enthält einen Quellgenerator, der das GeneratedRegexAttribute Attribut an einer partiellen Methode erkennt, die Regex zurückgibt. Ab .NET 9 kann das Attribut auch auf partielle Eigenschaften angewendet werden. Der Quellgenerator liefert eine Implementierung der Methode oder Eigenschaft, die die gesamte Logik für das Regex enthält. Beispielsweise könnten Sie zuvor Code wie folgt geschrieben haben:
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
}
}
Um den Quellgenerator zu verwenden, schreiben Sie den vorherigen Code wie folgt um:
[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
}
}
Ab .NET 9 können Sie die GeneratedRegexAttribute Eigenschaft auch auf eine partielle Eigenschaft anstelle einer partiellen Methode anwenden. Dies wird durch die Unterstützung von C# 13 für partielle Eigenschaften aktiviert. Das folgende Beispiel zeigt das entsprechende Merkmal:
[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
}
}
Tipp
Das Flag RegexOptions.Compiled wird vom Quellgenerator ignoriert, sodass es in der quellgenerierten Version nicht benötigt wird.
Die generierte Implementierung von AbcOrDefGeneratedRegex() speichert auf ähnliche Weise eine Singleton-Regex-Instanz zwischen, sodass keine zusätzliche Zwischenspeicherung erforderlich ist, um Code zu nutzen.
Die folgende Abbildung ist ein Abbild der vom Quellgenerator erzeugten zwischengespeicherten Instanz, internal zur Regex Unterklasse, die der Quellgenerator ausgibt.
Aber wie Sie sehen, wird nicht nur new Regex(...) ausgeführt. Vielmehr generiert der Quellgenerator eine benutzerdefinierte Implementierung, die von Regex abgeleitet ist und als C#-Code mit einer ähnlichen Logik aufgebaut ist wie der von RegexOptions.Compiled generierte Zwischencode. Sie erhalten alle Durchsatzleistungsvorteile von RegexOptions.Compiled (tatsächlich sogar noch mehr) sowie die Startvorteile von Regex.CompileToAssembly, aber ohne die Komplexität von CompileToAssembly. Die ausgegebene Quelle ist Teil Ihres Projekts, was bedeutet, dass sie auch mühelos angezeigt und debuggt werden kann.
Tipp
Klicken Sie in Visual Studio mit der rechten Maustaste auf die partielle Methoden- oder Eigenschaftendeklaration, und wählen Sie "Gehe zu Definition" aus. Alternativ können Sie im Projektmappen-Explorer den Projektknoten auswählen und dann Abhängigkeiten>Analyser>System.Text.RegularExpressions.Generator>System.Text.RegularExpressions.Generator.RegexGenerator>RegexGenerator.g.cs erweitern, um den generierten C#-Code dieses Regex-Generators anzuzeigen.
Sie können darin Breakpoints festlegen, sie schrittweise durchlaufen und sie als Lerntool nutzen, um genau nachzuvollziehen, wie die Engine für reguläre Ausdrücke Ihr Muster mit Ihrer Eingabe verarbeitet. Der Generator generiert sogar XML-Kommentare mit drei Schrägstrichen, um den Ausdruck und seine Verwendung auf einen Blick verständlich zu machen.
In den quellgenerierten Dateien
Mit .NET 7 wurden sowohl der Quellgenerator als auch RegexCompiler fast vollständig neu geschrieben, wodurch sich die Struktur des generierten Codes grundlegend geändert hat. Dieser Ansatz wurde erweitert, um alle Konstrukte zu behandeln (mit einer Einschränkung), und sowohl RegexCompiler als auch der Quellgenerator stimmen nach dem neuen Ansatz größtenteils 1:1 miteinander überein. Betrachten Sie die Ausgabe des Quellgenerators für eine der primären Funktionen aus dem Ausdruck 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;
}
Das Ziel des quellgenerierten Codes besteht darin, verständlich zu sein – mit einer gut nachvollziehbaren Struktur, mit Kommentaren, die die Vorgänge in den einzelnen Schritten erklären, und im Allgemeinen mit Code, der so ausgeben wird, als hätte ihn ein Mensch geschrieben. Selbst wenn Backtracking involviert ist, wird die Struktur des Backtracking Teil der Codestruktur, anstatt sich bei der Angabe des nächsten Sprungziels auf einen Stack zu verlassen. Hier sehen Sie beispielsweise den Code für die gleiche generierte Abgleichsfunktion, wenn der Ausdruck [ab]*[bc] lautet:
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;
}
Sie können die Rückverfolgungsstruktur im Code sehen, wobei ein CharLoopBacktrack Label ausgegeben wird, um anzuzeigen, wohin zurückverfolgt werden soll, und goto verwendet wird, um zu dieser Stelle zu springen, wenn ein nachfolgender Teil des regulären Ausdrucks fehlschlägt.
Der Code für die Implementierung von RegexCompiler und der Quellcodegenerator sehen sehr ähnlich aus: ähnlich benannte Methoden, ähnliche Aufrufstruktur und sogar ähnliche Kommentare in der gesamten Implementierung. Sie führen größtenteils zu identischem Code, einmal in IL und einmal in C#. Natürlich ist der C#-Compiler dann für die Übersetzung von C# in IL (Zwischensprache) verantwortlich, sodass das resultierende IL in beiden Fällen wahrscheinlich nicht identisch ist. Der Quellgenerator verlässt sich in verschiedenen Fällen darauf und nutzt die Tatsache, dass der C#-Compiler verschiedene C#-Konstrukte weiter optimiert. Daher gibt es einige spezifische Dinge, für die der Quellgenerator einen besser optimierten Abgleichscode erzeugt als RegexCompiler. In einem der vorherigen Beispiele sehen Sie beispielsweise, dass der Quellgenerator eine Anweisung vom Typ „switch“ mit einer Verzweigung für 'a' und einer anderen Verzweigung für 'b' ausgibt. Da der C#-Compiler sehr gut darin ist, Switch-Anweisungen zu optimieren und über mehrere Strategien verfügt, um dies effizient zu tun, verfügt der Quellgenerator über eine spezielle Optimierung, die RegexCompiler nicht bietet. Bei Alternierungen betrachtet der Quellgenerator alle Verzweigungen, und wenn er nachweisen kann, dass jede Verzweigung mit einem anderen Startzeichen beginnt, gibt er eine Anweisung vom Typ „switch“ über dieses erste Zeichen aus und vermeidet die Ausgabe von Rückverfolgungscode für diese Alternierung.
Hier sehen Sie ein etwas komplizierteres Beispiel dafür. Alternierungen werden ausführlicher analysiert, um zu ermitteln, ob sie so umgestaltet werden können, dass sie durch die Rückverfolgungs-Engines leichter optimiert werden können, was zu einfacherem quellgeneriertem Code führt. Eine dieser Optimierungen unterstützt das Extrahieren gemeinsamer Präfixe aus Verzweigungen, und falls die Alternation atomar ist, sodass die Reihenfolge keine Rolle spielt, auch die Umordnung der Verzweigungen, um mehr solcher Extraktionen zu ermöglichen. Die Auswirkungen davon sind beim Wochentagsmuster Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday erkennbar, das eine Abgleichsfunktion wie die folgende erzeugt:
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;
}
Der Quellgenerator muss sich allerdings mit anderen Problemen auseinandersetzen, die bei der direkten Ausgabe in die Zwischensprache (IL) schlichtweg nicht existieren. In einem der Codebeispiele weiter oben finden Sie ein paar Klammern, die etwas seltsam auskommentiert sind. Das ist kein Fehler. Der Quellgenerator erkennt, dass, wenn diese Klammern nicht auskommentiert wären, die Struktur der Rückverfolgung vorsieht, von außerhalb des Bereichs zu einem innerhalb dieses Bereichs definierten Label zu springen; ein solches Label wäre für ein derartiges goto-Element nicht sichtbar, und der Code würde nicht kompiliert werden. Daher muss der Quellgenerator vermeiden, dass ein Bereich im Weg ist. In einigen Fällen wird der Bereich wie hier einfach auskommentiert. In anderen Fällen, in denen das nicht möglich ist, kann es manchmal Konstrukte vermeiden, die einen Gültigkeitsbereich erfordern (z. B. ein mehrzeiliger Anweisungsblock vom Typ if), wenn dies problematisch wäre.
Der Quellgenerator behandelt alles, was auch von RegexCompiler behandelt wird – mit einer Ausnahme: Genau wie bei der Behandlung von RegexOptions.IgnoreCase verwenden die Implementierungen jetzt eine Tabelle für die Groß- und Kleinschreibung, um Gruppen zur Erstellungszeit zu generieren, und geben an, wie diese Groß- und Kleinschreibungstabelle bei IgnoreCase-Rückverweisabgleichen herangezogen werden muss. Bei dieser Tabelle handelt es sich um eine interne Tabelle für System.Text.RegularExpressions.dll, und zumindest im Moment hat der externe Code für diese Assembly (einschließlich Code, der vom Quellgenerator ausgegeben wird) keinen Zugriff darauf. Das macht die Behandlung von IgnoreCase-Rückverweisen im Quellgenerator zu einer Herausforderung, und sie werden nicht unterstützt. Dies ist das eine Konstrukt, das von RegexCompiler unterstützt wird, aber nicht vom Quellgenerator. Wenn Sie versuchen, ein Muster mit einem dieser Elemente zu verwenden (was selten ist), gibt der Quellgenerator keine benutzerdefinierte Implementierung aus und greift stattdessen auf das Zwischenspeichern einer regulären Regex-Instanz zurück.
Das neue Element RegexCompiler wird zudem weder von RegexOptions.NonBacktracking noch vom Quellgenerator unterstützt. Wenn Sie RegexOptions.Compiled | RegexOptions.NonBacktracking angeben, wird das Flag Compiled einfach ignoriert, und wenn Sie NonBacktracking für den Quellgenerator angeben, wird auf ähnliche Weise eine reguläre Regex-Instanz zwischengespeichert.
Wann es zu verwenden ist
Die allgemeine Empfehlung lautet: Wenn Sie den Quellgenerator verwenden können, verwenden Sie ihn. Wenn Sie aktuell Regex in C# mit zur Kompilierzeit bekannten Argumenten verwenden, empfiehlt sich die Verwendung des Quellgenerators – insbesondere, wenn Sie bereits RegexOptions.Compiled verwenden (da der reguläre Ausdruck als Hotspot identifiziert wurde, der von einem höheren Durchsatz profitieren würde). Der Quellcode-Generator gibt Ihrem Regex folgende Vorteile:
- Alle Durchsatzvorteile von
RegexOptions.Compiled - Die Vorteile eines Startups, nicht alle Regex-Parsing, Analyse und Kompilierung zur Laufzeit durchführen zu müssen.
- Die Möglichkeit, die Vorabkompilierung mit dem für den Regex generierten Code zu nutzen.
- Bessere Debugging-Fähigkeit und besseres Verständnis des regulären Ausdrucks
- Die Möglichkeit, die Größe Ihrer bereits gekürzten App weiter zu reduzieren, indem Sie große Codeteile ausschneiden, die mit
RegexCompilerzusammenhängen (und möglicherweise sogar die Reflexionsemission selbst).
Bei Verwendung mit einer Option wie RegexOptions.NonBacktracking, für die der Quellgenerator keine benutzerdefinierte Implementierung generieren kann, werden trotzdem Zwischenspeicherung und XML-Kommentare ausgegeben, die die Implementierung beschreiben, um einen Mehrwert zu generieren Der größte Nachteil des Quellgenerators ist, dass er zusätzlichen Code an Ihre Assembly ausgibt, was potenziell zu einer Vergrößerung führt. Je mehr reguläre Ausdrücke sich in Ihrer App befinden und je größer sie sind, desto mehr Code wird für sie ausgegeben. In einigen Situationen ist der Quellgenerator genau wie RegexOptions.Compiled möglicherweise auch unnötig. Wenn Sie beispielsweise über einen regulären Ausdruck verfügen, der nur selten benötigt wird und bei dem der Durchsatz keine Rolle spielt, kann es vorteilhafter sein, sich für diese sporadische Verwendung nur auf den Interpreter zu verlassen.
Wichtig
.NET 7 enthält einen Analyzer, der die Verwendung von Regex identifiziert, die in den Quellgenerator konvertiert werden kann, und einen Fixer, der die Konvertierung für Sie durchführt.