Teilen über


Quellgeneratoren für reguläre .NET-Ausdrücke

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 die Gültigkeit des Musters sicherzustellen und um es in eine interne Struktur zu transformieren, die den analysierten regulären Ausdruck darstellt. Die Struktur wird dann auf verschiedene Arten optimiert. Hierzu wird das Muster in eine funktional äquivalente Variante umgewandelt, die effizienter ausgeführt werden kann. Die Struktur wird in ein Format geschrieben, das als eine Reihe von Opcodes und Operanden interpretiert werden kann, um der Interpreter-Engine für reguläre Ausdrücke mitzuteilen, wie der Abgleich erfolgen soll. Wenn ein Abgleich durchgeführt wird, werden diese Anweisungen einfach vom Interpreter durchlaufen und für den Eingabetext 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 reflektionsausgabebasierten Compiler zu Zwischenspracheanweisungen weitertransformiert und dann in einige DynamicMethod-Objekte geschrieben. Wenn eine Übereinstimmung ausgeführt wird, werden diese DynamicMethod-Methoden aufgerufen. Diese Zwischensprache tut im Wesentlichen genau das, was der Interpreter tut, nur eben speziell für das exakte Muster, das verarbeitet wird. Wenn das Muster z. B. [ac] enthält, würde der Interpreter einen Opcode sehen, der besagt: „entspricht dem Eingabezeichen an der aktuellen Position mit dem in dieser Satzbeschreibung angegebenen Satz“. Während die kompilierte Zwischensprache Code enthalten würde, der effektiv besagt: „entspricht dem Eingabezeichen an der aktuellen Position gegenüber 'a' oder 'c'“. Dieser Spezialfall und die Möglichkeit, Optimierungen basierend auf der Kenntnis des Musters durchzuführen, sind zwei der Hauptgründe dafür, dass sich durch die Angabe von RegexOptions.Compiled im Vergleich zum Interpreter eine viel höhere Abgleichsgeschwindigkeit und somit ein höherer Durchsatz erzielen lässt.

RegexOptions.Compiled hat mehrere Nachteile. Am meisten Auswirkungen hat, dass die Konstruktion kostspielig ist. Es fällt nicht nur der gleiche Aufwand an wie beim Interpreter, es müssen auch noch die resultierende RegexNode-Struktur und die generierten Opcodes/Operanden in Zwischensprache 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 (Version 7 und höher) enthält einen Quellgenerator, der das Attribut GeneratedRegexAttribute für eine partielle Methode erkennt, die Regex zurückgibt. Der Quellgenerator stellt eine Implementierung dieser Methode bereit, welche die gesamte Logik für Regex beinhaltet. 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
    }
}

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 eine Bildschirmaufnahme der zwischengespeicherten Quellinstanz, internal für die Regex Unterklasse, die der Quellgenerator ausgibt:

Zwischengespeichertes statisches RegEx-Feld

Aber wie Sie sehen, wird nicht nur new Regex(...) ausgeführt. Vielmehr wird vom Quellgenerator eine benutzerdefinierte, von Regex abgeleitete Implementierung als C#-Code mit einer ähnlichen Logik ausgegeben wie von RegexOptions.Compiled in der Zwischensprache. 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.

Debuggen über quellgenerierten RegEx-Code

Tipp

Klicken Sie in Visual Studio mit der rechten Maustaste auf die Deklaration Ihrer partiellen Methode, und wählen Sie Zu Definition wechseln aus. Alternativ können Sie auch im Projektmappen-Explorer den Projektknoten auswählen und dann Abhängigkeiten>Analysetools>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.

Generierte XML-Kommentare zur Beschreibung des regulären Ausdrucks

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 RegexCompiler sowie der Quellgenerator sind auch bei dem neuen Ansatz immer noch größtenteils identisch. 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;
}
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
    int pos = base.runtextpos;
    int matchStart = pos;
    int capture_starting_pos = 0;
    ReadOnlySpan<char> slice = inputSpan.Slice(pos);

    // 1st capture group.
    //{
        capture_starting_pos = pos;

        // Match with 2 alternative expressions.
        //{
            if (slice.IsEmpty)
            {
                UncaptureUntil(0);
                return false; // The input didn't match.
            }

            switch (slice[0])
            {
                case 'a':
                    pos++;
                    slice = inputSpan.Slice(pos);
                    break;

                case 'b':
                    // Match 'c'.
                    if ((uint)slice.Length < 2 || slice[1] != 'c')
                    {
                        UncaptureUntil(0);
                        return false; // The input didn't match.
                    }

                    pos += 2;
                    slice = inputSpan.Slice(pos);
                    break;

                default:
                    UncaptureUntil(0);
                    return false; // The input didn't match.
            }
        //}

        base.Capture(1, capture_starting_pos, pos);
    //}

    // Match 'd'.
    if (slice.IsEmpty || slice[0] != 'd')
    {
        UncaptureUntil(0);
        return false; // The input didn't match.
    }

    // The input matched.
    pos++;
    base.runtextpos = pos;
    base.Capture(0, matchStart, pos);
    return true;

    // <summary>Undo captures until it reaches the specified capture position.</summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    void UncaptureUntil(int capturePosition)
    {
        while (base.Crawlpos() > capturePosition)
        {
            base.Uncapture();
        }
    }
}

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 im Falle einer Rückverfolgung wird die Struktur der Rückverfolgung Teil der Codestruktur, anstatt sich bei der Angabe des nächsten Sprungziels auf einen Stapel 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;
}
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("ABab");
        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("BCbc")) < 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 sehen die Rückverfolgungsstruktur im Code, mit der ausgegebenen Bezeichnung CharLoopBacktrack für das Ziel der Rückverfolgung und mit goto, um zu dieser Stelle zu springen, wenn ein nachfolgender Teil des regulären Ausdrucks nicht erfolgreich ist.

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, wenn auch einmal in Zwischensprache und einmal in C#. Natürlich ist der C#-Compiler dann für die Übersetzung von C# in Zwischensprache verantwortlich, sodass die resultierende Zwischensprache in den 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 derartige Anweisungen sehr gut optimieren kann und über mehrere Strategien für eine effiziente Optimierung verfügt, profitiert der Quellgenerator von einer speziellen Optimierung, die RegexCompiler nicht zur Verfügung steht. Bei Alternierungen betrachtet der Quellgenerator alle Verzweigungen. Kann er nicht nachweisen, dass jede Verzweigung mit einem anderen Startzeichen beginnt, gibt er für dieses erste Zeichen eine Anweisung vom Typ „switch“ 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 allgemeiner Präfixe aus Branches, und falls die Alternierung atomisch ist, sodass die Reihenfolge keine Rolle spielt, auch die Neuanordnung von Verzweigungen, um weitere derartige 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;
}
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
    int pos = base.runtextpos;
    int matchStart = pos;
    ReadOnlySpan<char> slice = inputSpan.Slice(pos);

    // Match with 5 alternative expressions, atomically.
    {
        if (slice.IsEmpty)
        {
            return false; // The input didn't match.
        }

        switch (slice[0])
        {
            case 'M':
                // Match the string "onday".
                if (!slice.Slice(1).StartsWith("onday"))
                {
                    return false; // The input didn't match.
                }

                pos += 6;
                slice = inputSpan.Slice(pos);
                break;

            case 'T':
                // Match with 2 alternative expressions, atomically.
                {
                    if ((uint)slice.Length < 2)
                    {
                        return false; // The input didn't match.
                    }

                    switch (slice[1])
                    {
                        case 'u':
                            // Match the string "esday".
                            if (!slice.Slice(2).StartsWith("esday"))
                            {
                                return false; // The input didn't match.
                            }

                            pos += 7;
                            slice = inputSpan.Slice(pos);
                            break;

                        case 'h':
                            // Match the string "ursday".
                            if (!slice.Slice(2).StartsWith("ursday"))
                            {
                                return false; // The input didn't match.
                            }

                            pos += 8;
                            slice = inputSpan.Slice(pos);
                            break;

                        default:
                            return false; // The input didn't match.
                    }
                }

                break;

            case 'W':
                // Match the string "ednesday".
                if (!slice.Slice(1).StartsWith("ednesday"))
                {
                    return false; // The input didn't match.
                }

                pos += 9;
                slice = inputSpan.Slice(pos);
                break;

            case 'F':
                // Match the string "riday".
                if (!slice.Slice(1).StartsWith("riday"))
                {
                    return false; // The input didn't match.
                }

                pos += 6;
                slice = inputSpan.Slice(pos);
                break;

            case 'S':
                // Match with 2 alternative expressions, atomically.
                {
                    if ((uint)slice.Length < 2)
                    {
                        return false; // The input didn't match.
                    }

                    switch (slice[1])
                    {
                        case 'a':
                            // Match the string "turday".
                            if (!slice.Slice(2).StartsWith("turday"))
                            {
                                return false; // The input didn't match.
                            }

                            pos += 8;
                            slice = inputSpan.Slice(pos);
                            break;

                        case 'u':
                            // Match the string "nday".
                            if (!slice.Slice(2).StartsWith("nday"))
                            {
                                return false; // The input didn't match.
                            }

                            pos += 6;
                            slice = inputSpan.Slice(pos);
                            break;

                        default:
                            return false; // The input didn't match.
                    }
                }

                break;

            default:
                return false; // The input didn't match.
        }
    }

    // The input matched.
    base.runtextpos = pos;
    base.Capture(0, matchStart, pos);
    return true;
}

Wie Sie sehen, wurde Thursday neu sortiert und direkt nach Tuesday platziert, und für das Paar Tuesday/Thursday sowie für das Paar Saturday/Sunday sind mehrere switch-Ebenen vorhanden. Wenn Sie im Extremfall eine lange Alternierung vieler verschiedener Wörter erstellen, gibt der Quellgenerator letztendlich das logische Äquivalent eines Tries^1 aus, bei dem jedes Zeichen gelesen und per switch zu der Verzweigung gewechselt wird, um den Rest des Worts zu behandeln. Diese sehr effiziente Methode zum Abgleichen von Wörtern wird hier vom Quellgenerator verwendet.

Der Quellgenerator hat allerdings mit anderen Problemen zu kämpfen, die bei der direkten Ausgabe in Zwischensprache einfach nicht auftreten. In einem der Codebeispiele weiter oben finden Sie ein paar Klammern, die etwas seltsam auskommentiert sind. Das ist kein Fehler. Der Quellgenerator erkennt, dass die Struktur der Rückverfolgung vorsieht, von außerhalb des Bereichs zu einer innerhalb des Bereichs definierten Bezeichnung zu springen, wenn diese Klammern nicht auskommentiert wären. Eine solche Bezeichnung wäre für ein derartiges goto-Element nicht sichtbar, und der Code könnte 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. Sollte das nicht möglich sein, werden manchmal Konstrukte vermieden, die Bereiche erfordern (z. B. ein Block mit mehreren Anweisungen 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 einzige Konstrukt, das von RegexCompiler, aber nicht vom Quellgenerator unterstützt wird. Wenn Sie versuchen, ein Muster mit einem dieser Elemente zu verwenden (was selten ist), gibt der Quellgenerator keine benutzerdefinierte Implementierung aus und speichert stattdessen eine reguläre Regex-Instanz zwischen:

Nicht unterstützter regulärer Ausdruck, der trotzdem zwischengespeichert wird

Das neue Element RegexOptions.NonBacktracking wird zudem weder von RegexCompiler 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.

Einsatzgebiete

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 Quellgenerator bietet folgende Vorteile für Ihre regulären Ausdrücke:

  • Alle Durchsatzvorteile von RegexOptions.Compiled
  • Die Vorteile beim Start (also dass nicht die gesamten Analysen für die regulären Ausdrücke sowie die Kompilierung zur Laufzeit durchgeführt werden müssen)
  • Die Option, die Vorabkompilierung mit dem Code zu verwenden, der für den regulären Ausdruck generiert wurde
  • Bessere Debugging-Fähigkeit und besseres Verständnis des regulären Ausdrucks
  • Die Möglichkeit, Ihre gekürzte App zu verkleinern, indem Sie große Codeteile kürzen, die mit RegexCompiler zusammenhängen (und möglicherweise sogar die Reflektionsausgabe 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 ein Analysetool, das die Verwendung von Regex identifiziert, die in den Quellgenerator konvertiert werden kann, sowie eine Korrekturregel, die die Konvertierung für Sie durchführt:

Analysetool und Korrekturregel für RegexGenerator

Siehe auch