Sdílet prostřednictvím


Generátory zdrojů regulárních výrazů .NET

Regulární výraz neboli regulární výraz je řetězec, který vývojářům umožňuje vyjádřit hledaný vzor, což představuje běžný způsob, jak hledat text a extrahovat výsledky jako podmnožinu z hledaného řetězce. V .NET se System.Text.RegularExpressions obor názvů používá k definování Regex instancí a statických metod a porovnávání s uživatelsky definovanými vzory. V tomto článku se dozvíte, jak pomocí generování zdrojového kódu generovat Regex instance pro optimalizaci výkonu.

Poznámka:

Pokud je to možné, místo kompilování regulárních výrazů pomocí RegexOptions.Compiled možnosti používejte zdrojové generované regulární výrazy. Generování zdrojového kódu může vaší aplikaci pomoct rychleji, rychleji spustit a lépe se oříznout. Informace o tom, kdy je možné generování zdroje, najdete v tématu Kdy ji použít.

Kompilované regulární výrazy

Když píšete new Regex("somepattern"), stane se pár věcí. Zadaný vzor se analyzuje, a to jak k zajištění platnosti vzoru, tak k jeho transformaci na interní strom, který představuje parsovaný regulární výraz. Strom se pak optimalizuje různými způsoby a transformuje model na funkčně ekvivalentní variantu, která se dá efektivněji spustit. Strom se zapíše do formuláře, který lze interpretovat jako řadu opcode a operandů, které poskytují pokyny interpretačnímu stroji regulárních výrazů, jak se shodovat. Když se provede shoda, interpret je jednoduše provede těmito pokyny a zpracuje je proti vstupnímu textu. Při vytváření instance nové Regex instance nebo volání jedné ze statických metod Regexje interpret použit výchozí modul.

Při zadání RegexOptions.Compiledse provede všechna stejná stavební práce. Výsledné instrukce se dále transformují kompilátorem založeným na reflexi na instrukce IL, které jsou zapsány do několika DynamicMethod objektů. Při provedení shody jsou tyto DynamicMethod metody vyvolány. Tento il v podstatě dělá přesně to, co by interpret udělal, s výjimkou specializovaného na přesný vzor, který se zpracovává. Pokud například vzor obsahuje [ac], interpret by viděl opcode, který říká "shoda vstupního znaku na aktuální pozici proti sadě zadané v tomto popisu sady". Zatímco zkompilovaná il by obsahovala kód, který efektivně říká, "odpovídá vstupnímu znaku na aktuální pozici proti 'a' nebo 'c'". Tato speciální velikost a schopnost provádět optimalizace na základě znalostí modelu jsou některé z hlavních důvodů, které určují RegexOptions.Compiled mnohem rychleji odpovídající propustnost než interpret.

Existuje několik nevýhod RegexOptions.Compiled. Nejvýraznější je, že je nákladné sestavit. Nejen že jsou všechny stejné náklady zaplacené za interpreta, ale pak musí zkompilovat výsledný RegexNode strom a vygenerované opcodes/operandy do IL, což přidává nevýkonné výdaje. Vygenerované IL musí být dále zkompilovány JIT při prvním použití, což vede k ještě větším nákladům při spuštění. RegexOptions.Compiled představuje základní kompromis mezi režií při prvním použití a režijními náklady při každém následném použití. Použití System.Reflection.Emit také inhibuje použití RegexOptions.Compiled v určitých prostředích; některé operační systémy neumožňují dynamicky generovaný kód spustit a v takových systémech Compiled se stává no-op.

Generování zdroje

.NET 7 zavedl nový RegexGenerator generátor zdrojů. Generátor zdroje je komponenta, která se připojuje k kompilátoru a rozšiřuje kompilační jednotku o další zdrojový kód. Sada .NET SDK (verze 7 a novější) obsahuje generátor zdrojů, který rozpozná GeneratedRegexAttribute atribut částečné metody, která vrací Regex. Generátor zdroje poskytuje implementaci této metody, která obsahuje veškerou logiku Regexpro . Dříve jste například mohli napsat kód takto:

private static readonly Regex s_abcOrDefGeneratedRegex =
    new(pattern: "abc|def",
        options: RegexOptions.Compiled | RegexOptions.IgnoreCase);

private static void EvaluateText(string text)
{
    if (s_abcOrDefGeneratedRegex.IsMatch(text))
    {
        // Take action with matching text
    }
}

Pokud chcete použít generátor zdroje, přepíšete předchozí kód následujícím způsobem:

[GeneratedRegex("abc|def", RegexOptions.IgnoreCase, "en-US")]
private static partial Regex AbcOrDefGeneratedRegex();

private static void EvaluateText(string text)
{
    if (AbcOrDefGeneratedRegex().IsMatch(text))
    {
        // Take action with matching text
    }
}

Tip

Příznak RegexOptions.Compiled je ignorován zdrojovým generátorem, takže není potřeba ve zdrojové verzi vygenerované zdrojem.

Vygenerovaná implementace AbcOrDefGeneratedRegex() podobně ukládá jednu Regex instanci do mezipaměti, takže ke zpracování kódu není potřeba žádné další ukládání do mezipaměti.

Následující obrázek je snímek obrazovky zdrojové instance internal vygenerované v mezipaměti do Regex podtřídy, kterou generuje generátor zdrojů:

Statické pole regulárních výrazů uložených v mezipaměti

Ale jak je vidět, není to jen dělá new Regex(...). Zdrojový generátor spíše generuje jako kód jazyka C# vlastní Regeximplementaci odvozenou odvozující logiku podobnou tomu, co RegexOptions.Compiled generuje v IL. Získáte všechny výhody RegexOptions.Compiled výkonu propustnosti (ve skutečnosti) a výhody Regex.CompileToAssemblyspuštění , ale bez složitosti CompileToAssembly. Zdroj, který se vygeneruje, je součástí projektu, což znamená, že je také snadno zobrazitelný a laditelný.

Ladění prostřednictvím zdrojového kódu Regex

Tip

V sadě Visual Studio klikněte pravým tlačítkem na deklaraci částečné metody a vyberte Přejít k definici. Nebo případně vyberte uzel projektu v Průzkumník řešení a pak rozbalte závislosti>Analyzers>System.Text.RegularExpressions.Generator>System.Text.RegularExpressions.Generator.RegexGenerator>RegexGenerator.g.cs zobrazit vygenerovaný kód C# z tohoto generátoru regulárních výrazů.

V něm můžete nastavit zarážky, můžete je procházet a můžete ho použít jako výukový nástroj, abyste přesně pochopili, jak modul regulárních výrazů zpracovává váš vzor se vstupem. Generátor dokonce generuje komentáře trojité lomítko (XML), aby byl výraz srozumitelný na první pohled a tam, kde se používá.

Generované komentáře XML popisující regex

Uvnitř zdrojových souborů

S rozhraním .NET 7 se zdrojový generátor i RegexCompiler téměř zcela přepsal, v podstatě se změnila struktura generovaného kódu. Tento přístup byl rozšířen tak, aby zpracovával všechny konstrukce (s jedním upozorněním) a RegexCompiler zdrojový generátor se stále mapuje většinou 1:1 s ostatními, a to po novém přístupu. Představte si výstup generátoru zdroje pro jednu z primárních funkcí z výrazu abc|def :

private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
    int pos = base.runtextpos;
    int matchStart = pos;
    ReadOnlySpan<char> slice = inputSpan.Slice(pos);

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

        switch (slice[0])
        {
            case 'A' or 'a':
                if ((uint)slice.Length < 3 ||
                    !slice.Slice(1).StartsWith("bc", StringComparison.OrdinalIgnoreCase)) // Match the string "bc" (ordinal case-insensitive)
                {
                    return false; // The input didn't match.
                }

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

            case 'D' or 'd':
                if ((uint)slice.Length < 3 ||
                    !slice.Slice(1).StartsWith("ef", StringComparison.OrdinalIgnoreCase)) // Match the string "ef" (ordinal case-insensitive)
                {
                    return false; // The input didn't match.
                }

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

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

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

Cílem zdrojového vygenerovaného kódu je pochopitelný, s snadno sledovatelnou strukturou, s komentáři vysvětlující, co se provádí v jednotlivých krocích, a obecně s kódem vygenerovaným v rámci hlavního principu, že generátor by měl generovat kód, jako by ho napsal člověk. I když je zpětné navracení zapojeno, struktura zpětného navracení se stane součástí struktury kódu, místo aby se spoléhala na zásobník, aby bylo zřejmé, kam se má přejít. Tady je například kód pro stejnou vygenerovanou odpovídající funkci, když je [ab]*[bc]výraz:

private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
    int pos = base.runtextpos;
    int matchStart = pos;
    int charloop_starting_pos = 0, charloop_ending_pos = 0;
    ReadOnlySpan<char> slice = inputSpan.Slice(pos);

    // Match a character in the set [ABab] greedily any number of times.
    //{
        charloop_starting_pos = pos;

        int iteration = slice.IndexOfAnyExcept(Utilities.s_ascii_600000006000000);
        if (iteration < 0)
        {
            iteration = slice.Length;
        }

        slice = slice.Slice(iteration);
        pos += iteration;

        charloop_ending_pos = pos;
        goto CharLoopEnd;

        CharLoopBacktrack:

        if (Utilities.s_hasTimeout)
        {
            base.CheckTimeout();
        }

        if (charloop_starting_pos >= charloop_ending_pos ||
            (charloop_ending_pos = inputSpan.Slice(charloop_starting_pos, charloop_ending_pos - charloop_starting_pos).LastIndexOfAny(Utilities.s_ascii_C0000000C000000)) < 0)
        {
            return false; // The input didn't match.
        }
        charloop_ending_pos += charloop_starting_pos;
        pos = charloop_ending_pos;
        slice = inputSpan.Slice(pos);

        CharLoopEnd:
    //}

    // Advance the next matching position.
    if (base.runtextpos < pos)
    {
        base.runtextpos = pos;
    }

    // Match a character in the set [BCbc].
    if (slice.IsEmpty || ((uint)((slice[0] | 0x20) - 'b') > (uint)('c' - 'b')))
    {
        goto CharLoopBacktrack;
    }

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

Můžete vidět strukturu zpětného navracení v kódu s CharLoopBacktrack popiskem, který se vygeneruje pro umístění zpětného navracení a goto použitý k přeskočení na toto místo, když další část regulárního výrazu selže.

Pokud se podíváte na implementaci RegexCompiler kódu a generátor zdrojového kódu, budou vypadat velmi podobně: podobně pojmenované metody, podobná struktura volání a dokonce podobné komentáře v celé implementaci. Ve většině případů mají za následek stejný kód, i když jeden v IL a druhý v jazyce C#. Kompilátor jazyka C# je samozřejmě zodpovědný za překlad jazyka C# do IL, takže výsledný il v obou případech pravděpodobně nebude identický. Zdrojový generátor spoléhá na to v různých případech, přičemž využívá skutečnost, že kompilátor jazyka C# dále optimalizuje různé konstrukce jazyka C#. Existuje několik konkrétních věcí, které zdrojový generátor vytvoří optimalizovanější odpovídající kód, než dělá RegexCompiler. Například v jednom z předchozích příkladů můžete zobrazit zdrojový generátor vygenerující příkaz switch s jednou větví pro 'a' a jinou větví pro 'b'. Vzhledem k tomu, že kompilátor jazyka C# je velmi dobrý při optimalizaci příkazů přepínače, s několika strategiemi, které jsou k dispozici pro efektivní způsob, má generátor zdrojů speciální optimalizaci, která RegexCompiler ne. V případě alternací se zdrojový generátor podívá na všechny větve a pokud může prokázat, že každá větev začíná jiným počátečním znakem, vygeneruje příkaz switch přes tento první znak a vyhne se výstupu jakéhokoli kódu zpětného navracení tohoto alternace.

Tady je trochu složitější příklad. Alternace jsou silně analyzovány, aby se zjistilo, jestli je možné refaktorovat způsobem, který je snadněji optimalizuje moduly pro navracení a které vedou k jednoduššímu zdrojovému kódu. Jedna z těchto optimalizací podporuje extrahování běžných předpon z větví a pokud je alternace atomická tak, aby řazení nezáleželo, přeuspořádejte větve tak, aby umožňovaly další takovou extrakci. Můžete vidět dopad tohoto vzorce Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sundaypro následující pracovní den , který vytváří odpovídající funkci takto:

private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
    int pos = base.runtextpos;
    int matchStart = pos;
    char ch;
    ReadOnlySpan<char> slice = inputSpan.Slice(pos);

    // Match with 6 alternative expressions, atomically.
    {
        int alternation_starting_pos = pos;

        // Branch 0
        {
            if ((uint)slice.Length < 6 ||
                !slice.StartsWith("monday", StringComparison.OrdinalIgnoreCase)) // Match the string "monday" (ordinal case-insensitive)
            {
                goto AlternationBranch;
            }

            pos += 6;
            slice = inputSpan.Slice(pos);
            goto AlternationMatch;

            AlternationBranch:
            pos = alternation_starting_pos;
            slice = inputSpan.Slice(pos);
        }

        // Branch 1
        {
            if ((uint)slice.Length < 7 ||
                !slice.StartsWith("tuesday", StringComparison.OrdinalIgnoreCase)) // Match the string "tuesday" (ordinal case-insensitive)
            {
                goto AlternationBranch1;
            }

            pos += 7;
            slice = inputSpan.Slice(pos);
            goto AlternationMatch;

            AlternationBranch1:
            pos = alternation_starting_pos;
            slice = inputSpan.Slice(pos);
        }

        // Branch 2
        {
            if ((uint)slice.Length < 9 ||
                !slice.StartsWith("wednesday", StringComparison.OrdinalIgnoreCase)) // Match the string "wednesday" (ordinal case-insensitive)
            {
                goto AlternationBranch2;
            }

            pos += 9;
            slice = inputSpan.Slice(pos);
            goto AlternationMatch;

            AlternationBranch2:
            pos = alternation_starting_pos;
            slice = inputSpan.Slice(pos);
        }

        // Branch 3
        {
            if ((uint)slice.Length < 8 ||
                !slice.StartsWith("thursday", StringComparison.OrdinalIgnoreCase)) // Match the string "thursday" (ordinal case-insensitive)
            {
                goto AlternationBranch3;
            }

            pos += 8;
            slice = inputSpan.Slice(pos);
            goto AlternationMatch;

            AlternationBranch3:
            pos = alternation_starting_pos;
            slice = inputSpan.Slice(pos);
        }

        // Branch 4
        {
            if ((uint)slice.Length < 6 ||
                !slice.StartsWith("fr", StringComparison.OrdinalIgnoreCase) || // Match the string "fr" (ordinal case-insensitive)
                ((((ch = slice[2]) | 0x20) != 'i') & (ch != 'İ')) || // Match a character in the set [Ii\u0130].
                !slice.Slice(3).StartsWith("day", StringComparison.OrdinalIgnoreCase)) // Match the string "day" (ordinal case-insensitive)
            {
                goto AlternationBranch4;
            }

            pos += 6;
            slice = inputSpan.Slice(pos);
            goto AlternationMatch;

            AlternationBranch4:
            pos = alternation_starting_pos;
            slice = inputSpan.Slice(pos);
        }

        // Branch 5
        {
            // Match a character in the set [Ss].
            if (slice.IsEmpty || ((slice[0] | 0x20) != 's'))
            {
                return false; // The input didn't match.
            }

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

                switch (slice[1])
                {
                    case 'A' or 'a':
                        if ((uint)slice.Length < 8 ||
                            !slice.Slice(2).StartsWith("turday", StringComparison.OrdinalIgnoreCase)) // Match the string "turday" (ordinal case-insensitive)
                        {
                            return false; // The input didn't match.
                        }

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

                    case 'U' or 'u':
                        if ((uint)slice.Length < 6 ||
                            !slice.Slice(2).StartsWith("nday", StringComparison.OrdinalIgnoreCase)) // Match the string "nday" (ordinal case-insensitive)
                        {
                            return false; // The input didn't match.
                        }

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

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

        }

        AlternationMatch:;
    }

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

Ve stejnou dobu má zdrojový generátor další problémy, které se musí potýkat s tím, že při přímém výstupu do IL prostě neexistují. Pokud se podíváte na několik příkladů kódu zpět, můžete vidět některé složené závorky poněkud podivně okomentované. To není chyba. Generátor zdrojů rozpozná, že pokud tyto složené závorky nebyly okomentovány, struktura zpětného navracení se spoléhá na přeskakování mimo obor na popisek definovaný uvnitř tohoto oboru; takový popisek by takový popisek nebyl viditelný goto a kód by se nepodařilo zkompilovat. Zdrojový generátor se proto musí vyhnout rozsahu v cestě. V některých případech jednoduše zakomentuje rozsah, jak jsme tady udělali. V jiných případech, kdy to není možné, se někdy může vyhnout konstruktorům, které vyžadují obory (například blok s více příkazy if ), pokud by to bylo problematické.

Zdrojový generátor zpracovává všechno RegexCompiler , s jednou výjimkou. Stejně jako v případě zpracování RegexOptions.IgnoreCaseteď implementace používají k vygenerování sad v době výstavby tabulku s velikostí a způsob, jakým IgnoreCase porovnávání zpětného odvozování potřebuje prohlédnout tabulku s velikostí a velikostí. Tato tabulka je interní System.Text.RegularExpressions.dllpro toto sestavení (včetně kódu generovaného generátorem zdroje) a prozatím k němu nemá přístup aspoň kód, který je externí pro toto sestavení (včetně kódu generovaného generátorem zdroje). Díky tomu zpracování IgnoreCase backreference představuje problém ve zdrojovém generátoru a nejsou podporované. Jedná se o jeden konstruktor, který není podporován zdrojovým generátorem RegexCompiler, který je podporován . Pokud se pokusíte použít vzor, který má jednu z těchto (což je vzácné), generátor zdroje nevygeneruje vlastní implementaci a místo toho se vrátí do mezipaměti pravidelné Regex instance:

Nepodporované regulární výrazy se stále ukládají do mezipaměti

Také ani RegexCompiler generátor zdroje nepodporuje nový RegexOptions.NonBacktracking. Pokud zadáte RegexOptions.Compiled | RegexOptions.NonBacktracking, Compiled příznak se bude ignorovat a pokud zadáte NonBacktracking do zdrojového generátoru, bude se podobně vracet do mezipaměti běžné Regex instance.

Kdy ji použít

Obecné pokyny jsou, pokud můžete použít generátor zdrojů, použijte ho. Pokud používáte Regex dnes v jazyce C# s argumenty známými v době kompilace, a to zejména v případě, že už používáte RegexOptions.Compiled (protože regulární výraz byl identifikován jako aktivní bod, který by měl prospěch z rychlejší propustnosti), měli byste raději použít generátor zdrojů. Generátor zdrojů poskytne regulárnímu výrazu následující výhody:

  • Všechny výhody propustnosti .RegexOptions.Compiled
  • Výhody spuštění, které nemusí provádět všechny analýzy, analýzy a kompilace regulárních výrazů za běhu.
  • Možnost použít předem připravenou kompilaci s kódem vygenerovaným pro regulární výraz.
  • Lepší ladění a porozumění regulárnímu výrazu.
  • Možnost zmenšit velikost oříznuté aplikace oříznutím velkého množství kódu spojeného s RegexCompiler kódem (a potenciálně i reflexe se vygeneruje).

Při použití s možností, jako RegexOptions.NonBacktracking je například zdroj generátor nemůže vygenerovat vlastní implementaci, bude stále generovat ukládání do mezipaměti a komentáře XML popisující implementaci, což z něj dělá cenné. Hlavní nevýhodou zdrojového generátoru je, že do sestavení generuje další kód, takže existuje potenciál pro zvýšení velikosti. Čím více regulárních výrazů ve vaší aplikaci a čím větší jsou, tím více kódu se pro ně vygeneruje. V některých situacích, stejně jako RegexOptions.Compiled by to mohlo být zbytečné, takže také může být zdrojem generátoru. Pokud máte například regulární výraz, který je potřeba jen zřídka a pro kterou propustnost nezáleží, může být vhodnější jen spoléhat na interpreta pro toto sporadické použití.

Důležité

.NET 7 obsahuje analyzátor , který identifikuje použití Regex , které by bylo možné převést na generátor zdroje, a opravovač, který provede převod za vás:

Analyzátor regexGenerator a fixer

Viz také