Udostępnij za pośrednictwem


Generatory źródeł wyrażeń regularnych platformy .NET

Wyrażenie regularne lub wyrażenie regularne to ciąg, który umożliwia deweloperowi wyrażenie szukanego wzorca, dzięki czemu jest to typowy sposób wyszukiwania tekstu i wyodrębniania wyników jako podzestawu z wyszukiwanego ciągu. Na platformie .NET System.Text.RegularExpressions przestrzeń nazw służy do definiowania Regex wystąpień i metod statycznych oraz dopasowywania ich do wzorców zdefiniowanych przez użytkownika. W tym artykule dowiesz się, jak używać generowania źródła do generowania Regex wystąpień w celu optymalizacji wydajności.

Uwaga

Jeśli to możliwe, użyj wyrażeń regularnych generowanych przez źródło zamiast kompilowania wyrażeń regularnych przy użyciu RegexOptions.Compiled opcji . Generowanie źródła może pomóc aplikacji szybciej rozpocząć pracę, szybciej uruchomić i być bardziej przycinane. Aby dowiedzieć się, kiedy jest możliwe generowanie źródła, zobacz Kiedy go używać.

Skompilowane wyrażenia regularne

Gdy piszesz new Regex("somepattern"), zdarza się kilka rzeczy. Określony wzorzec jest analizowany, zarówno w celu zapewnienia ważności wzorca, jak i przekształcenia go w drzewo wewnętrzne reprezentujące przeanalizowane wyrażenie regularne. Drzewo jest następnie optymalizowane na różne sposoby, przekształcając wzorzec w równoważną funkcjonalnie odmianę, która może być wydajniej wykonywana. Drzewo jest zapisywane w formularzu, który może być interpretowany jako seria operacji i operandów, które dostarczają instrukcje dla aparatu interpretera wyrażeń regularnych na temat sposobu dopasowania. Po wykonaniu dopasowania interpreter po prostu przechodzi przez te instrukcje, przetwarzając je względem tekstu wejściowego. Podczas tworzenia wystąpienia nowego Regex wystąpienia lub wywoływania jednej z metod statycznych w interpreterze Regexjest używany domyślny aparat.

Po określeniu parametru RegexOptions.Compiledwszystkie te same prace budowlane są wykonywane. Wynikowe instrukcje są dalej przekształcane przez kompilator oparty na odbiciu do instrukcji IL, które są zapisywane w kilku DynamicMethod obiektach. Po wykonaniu dopasowania te DynamicMethod metody są wywoływane. To IL zasadniczo robi dokładnie to, co zrobiłby interpreter, z wyjątkiem wyspecjalizowanego do dokładnego przetwarzania wzorca. Jeśli na przykład wzorzec zawiera [ac], interpreter zobaczy kod opcode z informacją "dopasuj znak wejściowy w bieżącej pozycji względem zestawu określonego w tym opisie zestawu". Podczas gdy skompilowany il będzie zawierać kod, który skutecznie mówi: "dopasuj znak wejściowy w bieżącej pozycji względem 'a' lub 'c'". Ta specjalna wielkość liter i możliwość przeprowadzania optymalizacji na podstawie wiedzy o wzorcu są jednymi z głównych powodów, dla których określenie zapewnia znacznie szybsze dopasowywanie RegexOptions.Compiled przepływności niż interpreter.

Istnieje kilka wad do RegexOptions.Compiled. Najbardziej wpływ ma to na to, że jest kosztowna konstrukcja. Nie tylko wszystkie te same koszty są wypłacane jako interpreter, ale następnie muszą skompilować wynikowe RegexNode drzewo i wygenerować kody operacyjne/operandy do IL, co dodaje nietrywialne wydatki. Wygenerowany il musi być dodatkowo kompilowany w trybie JIT przy pierwszym użyciu, co prowadzi do jeszcze większego kosztu podczas uruchamiania. RegexOptions.Compiled reprezentuje podstawową kompromis między narzutami na pierwsze użycie i narzutami na każde kolejne użycie. Zastosowanie System.Reflection.Emit tej metody hamuje również użycie RegexOptions.Compiled w niektórych środowiskach. Niektóre systemy operacyjne nie zezwalają na dynamiczne generowanie kodu do wykonania, a w takich systemach Compiled staje się nieoperacyjny.

Generowanie źródła

Platforma .NET 7 wprowadziła nowy RegexGenerator generator źródeł. Generator źródła to składnik, który podłącza się do kompilatora i rozszerza jednostkę kompilacji przy użyciu dodatkowego kodu źródłowego. Zestaw .NET SDK (wersja 7 lub nowsza) zawiera generator źródła, który rozpoznaje GeneratedRegexAttribute atrybut w metodzie częściowej, która zwraca Regexwartość . Generator źródła udostępnia implementację tej metody, która zawiera całą logikę dla klasy Regex. Na przykład wcześniej można było napisać kod podobny do następującego:

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
    }
}

Aby użyć generatora źródłowego, zapisz ponownie poprzedni kod w następujący sposób:

[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
    }
}

Napiwek

Flaga RegexOptions.Compiled jest ignorowana przez generator źródła, dlatego nie jest potrzebna w wersji wygenerowanej przez źródło.

Wygenerowana implementacja AbcOrDefGeneratedRegex() podobnie buforuje pojedyncze Regex wystąpienie, więc do użycia kodu nie jest potrzebne żadne dodatkowe buforowanie.

Na poniższej ilustracji przedstawiono przechwytywanie ekranu wystąpienia wygenerowanego w pamięci podręcznej źródła do Regex podklasy emitowanej internal przez generator źródłowy:

Buforowane pole statyczne regex

Ale jak widać, to nie tylko robi new Regex(...). Zamiast tego generator źródła emituje jako kod C# niestandardową Regeximplementację pochodną z logiką podobną do tego, co RegexOptions.Compiled emituje w il. Uzyskasz wszystkie korzyści z RegexOptions.Compiled wydajności przepływności (w rzeczywistości więcej) i korzyści Regex.CompileToAssemblyz uruchamiania programu , ale bez złożoności programu CompileToAssembly. Źródło, które jest emitowane, jest częścią projektu, co oznacza, że można go również łatwo wyświetlać i debugować.

Debugowanie za pomocą kodu regularnego wygenerowanego przez źródło

Napiwek

W programie Visual Studio kliknij prawym przyciskiem myszy deklarację metody częściowej i wybierz polecenie Przejdź do definicji. Alternatywnie wybierz węzeł projektu w Eksplorator rozwiązań, a następnie rozwiń węzeł Dependencies>Analyzers>System.Text.RegularExpressions.Generator>System.Text.RegularExpressions.Generator.Generator.RegexGenerator>RegexGenerator.g.cs, aby wyświetlić wygenerowany kod języka C# z tego generatora wyrażeń regularnych.

Możesz ustawić w nim punkty przerwania, możesz przejść przez nie i użyć go jako narzędzia szkoleniowego, aby dokładnie zrozumieć, jak aparat wyrażeń regularnych przetwarza wzorzec z danymi wejściowymi. Generator generuje nawet komentarze potrójnego ukośnika (XML), aby ułatwić zrozumienie wyrażenia w skrócie i miejsce jego użycia.

Wygenerowane komentarze XML opisujące wyrażenie regularne

Wewnątrz plików wygenerowanych przez źródło

W przypadku platformy .NET 7 zarówno generator źródła, jak i RegexCompiler prawie całkowicie przepisano, zasadniczo zmieniając strukturę wygenerowanego kodu. To podejście zostało rozszerzone w celu obsługi wszystkich konstrukcji (z jednym zastrzeżeniem), a zarówno RegexCompiler generator źródła, jak i generator źródła nadal mapuje przede wszystkim 1:1 ze sobą, postępując zgodnie z nowym podejściem. Rozważ wyjście generatora źródłowego dla jednej z podstawowych funkcji z abc|def wyrażenia:

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;
}

Celem kodu generowanego przez źródło jest zrozumienie, z łatwą do naśladowania strukturą, z komentarzami wyjaśnianymi, co robi się w każdym kroku, a ogólnie z kodem emitowanym zgodnie z zasadą przewodnią, że generator powinien emitować kod tak, jakby człowiek go napisał. Nawet w przypadku wystąpienia wycofywania struktura wycofywania staje się częścią struktury kodu, a nie polegania na stosie, aby wskazać, gdzie przejść dalej. Na przykład oto kod dla tej samej wygenerowanej funkcji dopasowania, gdy wyrażenie to [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;
}

Możesz zobaczyć strukturę wycofywania w kodzie z etykietą emitowaną CharLoopBacktrack dla miejsca powrotu do i użytą goto do przejścia do tej lokalizacji, gdy kolejna część wyrażenia regularnego zakończy się niepowodzeniem.

Jeśli przyjrzysz się implementacji RegexCompiler kodu i generatorowi źródła, będą one wyglądać bardzo podobnie: podobnie nazwane metody, podobną strukturę wywołań, a nawet podobne komentarze w całej implementacji. W większości przypadków powodują one identyczny kod, choć jeden w IL i jeden w języku C#. Oczywiście kompilator języka C# jest wówczas odpowiedzialny za tłumaczenie języka C# na IL, więc wynikowe il w obu przypadkach prawdopodobnie nie będą identyczne. Generator źródła opiera się na tym w różnych przypadkach, korzystając z faktu, że kompilator języka C# będzie dodatkowo optymalizować różne konstrukcje języka C#. Istnieje kilka konkretnych rzeczy, które generator źródła generuje w ten sposób bardziej zoptymalizowany kod pasujący niż .RegexCompiler Na przykład w jednym z poprzednich przykładów można zobaczyć generator źródła emitujący instrukcję switch z jedną gałęzią dla 'a' i inną gałęzią dla 'b'elementu . Ponieważ kompilator języka C# jest bardzo dobry w optymalizowaniu instrukcji przełącznika, z wieloma strategiami umożliwiającymi efektywne wykonywanie tych czynności, generator źródła ma specjalną optymalizację, która RegexCompiler nie jest. W przypadku przemienności generator źródła analizuje wszystkie gałęzie i jeśli może udowodnić, że każda gałąź zaczyna się od innego znaku początkowego, emituje instrukcję switch dla tego pierwszego znaku i unika wyprowadzania dowolnego kodu wycofywania dla tej zmiany.

Oto nieco bardziej skomplikowany przykład tego. Zmiany są bardziej intensywnie analizowane w celu określenia, czy można je refaktoryzować w sposób, który ułatwi ich optymalizację przez aparaty wycofywania i doprowadzi to do prostszego kodu generowanego przez źródło. Jedna z takich optymalizacji obsługuje wyodrębnianie typowych prefiksów z gałęzi, a jeśli zmiana jest niepodzielna, tak aby kolejność nie ma znaczenia, zmień kolejność gałęzi, aby umożliwić bardziej takie wyodrębnianie. Możesz zobaczyć wpływ tego dla następującego wzorca Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sundaydni powszedniego, który generuje zgodną funkcję w następujący sposób:

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;
}

W tym samym czasie generator źródła ma inne problemy, aby zmagać się z tym, że po prostu nie istnieją podczas bezpośredniego wyprowadzania danych do IL. Jeśli spojrzysz wstecz na kilka przykładów kodu, zobaczysz, że niektóre nawiasy klamrowe nieco dziwnie skomentowane. To nie jest błąd. Generator źródła rozpoznaje, że jeśli te nawiasy klamrowe nie zostały skomentowane, struktura wycofywania polega na przechodzeniu poza zakres do etykiety zdefiniowanej w tym zakresie; taka etykieta nie byłaby widoczna dla takiego goto obiektu, a kompilowanie kodu zakończy się niepowodzeniem. W związku z tym generator źródła musi unikać bycia zakresem w ten sposób. W niektórych przypadkach po prostu oznaczy zakres jako komentarz w tym miejscu. W innych przypadkach, gdy nie jest to możliwe, czasami może unikać konstrukcji wymagających zakresów (takich jak blok z wieloma instrukcjami if ), jeśli tak byłoby problematyczne.

Generator źródła obsługuje wszystkie RegexCompiler obsługiwane elementy z jednym wyjątkiem. Podobnie jak w przypadku obsługi RegexOptions.IgnoreCase, implementacje używają teraz tabeli wielkości liter do generowania zestawów w czasie budowy i sposobu IgnoreCase dopasowania wstecznego musi skonsultować się z tabelą wielkości liter. Ta tabela jest wewnętrzna dla System.Text.RegularExpressions.dllelementu , a na razie co najmniej kod zewnętrzny dla tego zestawu (w tym kod emitowany przez generator źródła) nie ma dostępu do niego. Dzięki temu obsługa IgnoreCase backreferences jest wyzwaniem w generatorze źródłowym i nie są one obsługiwane. Jest to jedna konstrukcja nieobsługiwana przez generator źródła obsługiwany przez RegexCompilerprogram . Jeśli spróbujesz użyć wzorca, który ma jeden z tych (co jest rzadkie), generator źródła nie emituje niestandardowej implementacji i zamiast tego powróci do buforowania zwykłego Regex wystąpienia:

Nieobsługiwany rejestr nadal jest buforowany

Ponadto ani generator źródła nie RegexCompiler obsługuje nowego RegexOptions.NonBacktrackingelementu . Jeśli określisz RegexOptions.Compiled | RegexOptions.NonBacktrackingwartość , flaga Compiled będzie po prostu ignorowana, a jeśli określisz NonBacktracking generator źródła, będzie ona podobnie wracać do buforowania zwykłego Regex wystąpienia.

Zastosowanie

Ogólne wskazówki są następujące, jeśli możesz użyć generatora źródłowego, użyj go. Jeśli używasz Regex dzisiaj w języku C# z argumentami znanymi w czasie kompilacji, a zwłaszcza jeśli już używasz RegexOptions.Compiled (ponieważ wyrażenie regularne zostało zidentyfikowane jako punkt gorąca, które skorzystałoby z szybszej przepływności), wolisz użyć generatora źródłowego. Generator źródła daje regex następujące korzyści:

  • Wszystkie korzyści płynące z przepływności .RegexOptions.Compiled
  • Korzyści wynikające z uruchamiania nie muszą wykonywać wszystkich analiz wyrażeń regularnych, analizy i kompilacji w czasie wykonywania.
  • Opcja użycia kompilacji przed czasem z kodem wygenerowanym dla wyrażenia regularnego.
  • Lepsza możliwość debugowania i zrozumienie wyrażenia regularnego.
  • Możliwość zmniejszenia rozmiaru przyciętej aplikacji przez przycinanie dużych pokosów kodu skojarzonego z RegexCompiler (a potencjalnie nawet odbicie emituje się).

W przypadku użycia z opcją podobną RegexOptions.NonBacktracking do tego, dla której generator źródła nie może wygenerować implementacji niestandardowej, nadal będzie emitować buforowanie i komentarze XML opisujące implementację, co czyni go cennym. Głównym minusem generatora źródła jest to, że emituje dodatkowy kod do zestawu, więc istnieje potencjał zwiększenia rozmiaru. Im więcej wyrażeń regularnych w aplikacji i im większe, tym więcej kodu będzie dla nich emitowane. W niektórych sytuacjach, podobnie jak RegexOptions.Compiled może być niepotrzebne, więc też może być generator źródła. Jeśli na przykład masz wyrażenie regularne, które jest potrzebne tylko rzadko i dla którego przepływności nie ma znaczenia, może być bardziej korzystne, aby po prostu polegać na interpreterze dla tego sporadycznego użycia.

Ważne

Platforma .NET 7 zawiera analizator , który identyfikuje użycie Regex tego elementu, który może zostać przekonwertowany na generator źródła, oraz moduł naprawiający, który wykonuje konwersję:

Analizator regexGenerator i poprawka

Zobacz też