Källgeneratorer för reguljärt .NET-uttryck

Ett reguljärt uttryck, eller regex, är en sträng som gör det möjligt för en utvecklare att uttrycka ett mönster som söks efter, vilket gör det till ett mycket vanligt sätt att söka i text och extrahera resultat som en delmängd från den sökta strängen. I .NET System.Text.RegularExpressions används namnområdet för att definiera Regex instanser och statiska metoder och matcha användardefinierade mönster. I den här artikeln får du lära dig hur du använder källgenerering för att generera Regex instanser för att optimera prestanda.

Kommentar

Om möjligt använder du källgenererade reguljära uttryck i stället för att kompilera reguljära uttryck med hjälp av RegexOptions.Compiled alternativet . Källgenerering kan hjälpa din app att starta snabbare, köra snabbare och bli mer trimmad. Information om när källgenerering är möjligt finns i När du ska använda den.

Kompilerade reguljära uttryck

När du skriver new Regex("somepattern")händer några saker. Det angivna mönstret parsas, både för att säkerställa mönstrets giltighet och för att omvandla det till ett internt träd som representerar den parsade regexen. Trädet optimeras sedan på olika sätt och omvandlar mönstret till en funktionellt likvärdig variant som kan köras mer effektivt. Trädet skrivs till ett formulär som kan tolkas som en serie opcodes och operander som ger instruktioner till regex-tolkmotorn om hur du matchar. När en matchning utförs går tolken helt enkelt igenom dessa instruktioner och bearbetar dem mot indatatexten. När du instansierar en ny Regex instans eller anropar någon av de statiska metoderna på Regexär tolken standardmotorn som används.

När du anger RegexOptions.Compiledutförs samma byggtidsarbete. De resulterande instruktionerna skulle omvandlas ytterligare av den reflektionsbaserade kompilatorn till IL-instruktioner som skulle skrivas till några få DynamicMethods. När en matchning utfördes anropas dessa DynamicMethod. Denna IL skulle i princip göra exakt vad tolken skulle göra, med undantag för det exakta mönster som bearbetas. Om mönstret till exempel innehöll [ac]skulle tolken se ett opcode som sa "matcha indatatecknet vid den aktuella positionen mot den uppsättning som anges i den här uppsättningsbeskrivningen" medan den kompilerade IL:en skulle innehålla kod som i praktiken sa "matcha indatatecknet vid den aktuella positionen mot 'a' eller 'c'". Detta speciella hölje och möjligheten att utföra optimeringar baserat på kunskap om mönstret är några av de främsta orsakerna till att RegexOptions.Compiled ange avkastningar som är mycket snabbare matchande dataflöde än tolken.

Det finns flera nackdelar med .RegexOptions.Compiled Det mest effektfulla är att det medför mycket mer byggkostnader än att använda tolken. Det är inte bara samma kostnader som betalas för tolken, utan den måste sedan kompilera det resulterande RegexNode trädet och genererade opcodes/operander till IL, vilket lägger till icke-triviala kostnader. Den genererade IL:en måste ytterligare JIT-kompileras vid första användningen, vilket leder till ännu mer kostnader vid start. RegexOptions.Compiled representerar en grundläggande kompromiss mellan omkostnader vid den första användningen och omkostnaderna vid varje efterföljande användning. Användningen av System.Reflection.Emit hämmar också användningen av RegexOptions.Compiled i vissa miljöer. Vissa operativsystem tillåter inte att dynamiskt genererad kod körs, och på sådana system Compiled blir det en no-op.

Källgenerering

.NET 7 introducerade en ny RegexGenerator källgenerator. När C#-kompilatorn skrevs om som C#-kompilatorn "Roslyn" exponerades objektmodeller för hela kompileringspipelinen samt analysverktyg. På senare tid har Roslyn aktiverat källgeneratorer. Precis som en analysator är en källgenerator en komponent som ansluts till kompilatorn och får samma information som en analysator, men förutom att kunna generera diagnostik kan den även utöka kompileringsenheten med ytterligare källkod. .NET 7+ SDK innehåller en ny källgenerator som identifierar den nya GeneratedRegexAttribute på en partiell metod som returnerar Regex. Källgeneratorn tillhandahåller en implementering av metoden som implementerar all logik för Regex. Du kan till exempel ha skrivit kod så här:

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

Du kan nu skriva om den tidigare koden på följande sätt:

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

Den genererade implementeringen av AbcOrDefGeneratedRegex() cachelagrar på liknande sätt en singleton-instans Regex , så ingen ytterligare cachelagring krävs för att använda kod.

Dricks

Flaggan RegexOptions.Compiled ignoreras av källgeneratorn, vilket gör att den inte längre behövs i den källgenererade versionen.

Följande bild är en skärmdump av den källgenererade cachelagrade instansen till internal den Regex underklass som källgeneratorn genererar:

Cachelagrat statiskt regex-fält

Men som du kan se är det inte bara att göra new Regex(...). I stället genererar källgeneratorn som C#-kod en anpassad Regex-härledd implementering med logik som liknar vad som RegexOptions.Compiled genererar i IL. Du får alla prestandafördelar med RegexOptions.Compiled dataflöde (mer faktiskt) och startfördelarna med Regex.CompileToAssembly, men utan komplexiteten i CompileToAssembly. Källan som genereras är en del av projektet, vilket innebär att den också är lätt att se och koppla bort.

Felsöka via källgenererad Regex-kod

Dricks

I Visual Studio högerklickar du på din partiella metoddeklaration och väljer Gå till definition. Alternativt kan du välja projektnoden i Solution Explorer och sedan expandera Dependencies>Analyzeers>System.Text.RegularExpressions.Generator>System.Text.RegularExpressions.Generator.RegexGenerator>RegexGenerator.g.cs för att se den genererade C#-koden från den här regex-generatorn.

Du kan ange brytpunkter i den, du kan gå igenom den och du kan använda den som ett inlärningsverktyg för att förstå exakt hur regex-motorn bearbetar ditt mönster med dina indata. Generatorn genererar till och med XML-kommentarer (triple-slash) för att göra uttrycket lätt att förstå och var det används.

Genererade XML-kommentarer som beskriver regex

Inuti källgenererade filer

Med .NET 7 skrevs både källgeneratorn och RegexCompiler nästan helt om, vilket i grunden ändrade strukturen för den genererade koden. Den här metoden har utökats för att hantera alla konstruktioner (med en varning), och både RegexCompiler och källgeneratorn mappar fortfarande mestadels 1:1 med varandra, enligt den nya metoden. Överväg källgeneratorns utdata för en av de primära funktionerna från (a|bc)d uttrycket:

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

Målet med den källgenererade koden är att vara begriplig, med en lätt att följa struktur, med kommentarer som förklarar vad som görs i varje steg, och i allmänhet med kod som genereras enligt den vägledande principen att generatorn ska avge kod som om en människa hade skrivit den. Även när backtracking är involverad blir strukturen för backtracking en del av kodens struktur, snarare än att förlita sig på en stack för att ange var du ska hoppa härnäst. Här är till exempel koden för samma genererade matchningsfunktion när uttrycket är [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;
}
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;
}

Du kan se strukturen för backtracking i koden, med en CharLoopBacktrack etikett som genereras för var du ska backa till och en goto som används för att hoppa till den platsen när en efterföljande del av regex misslyckas.

Om du tittar på kodimplementeringen RegexCompiler och källgeneratorn ser de mycket lika ut: liknande namngivna metoder, liknande anropsstruktur och till och med liknande kommentarer under hela implementeringen. För det mesta resulterar de i identisk kod, om än en i IL och en i C#. Naturligtvis ansvarar C#-kompilatorn sedan för att översätta C# till IL, så den resulterande IL:en i båda fallen kommer sannolikt inte att vara identisk. Källgeneratorn förlitar sig på det i olika fall och drar nytta av det faktum att C#-kompilatorn ytterligare optimerar olika C#-konstruktioner. Det finns några specifika saker som källgeneratorn därmed kommer att producera mer optimerad matchningskod än vad som gör RegexCompiler. I ett av de föregående exemplen kan du till exempel se källgeneratorn som genererar en switch-instruktion, med en gren för 'a' och en annan gren för 'b'. Eftersom C#-kompilatorn är mycket bra på att optimera switch-instruktioner, med flera strategier till sitt förfogande för hur man gör det effektivt, har källgeneratorn en speciell optimering som inte gör det RegexCompiler . För alternationer tittar källgeneratorn på alla grenar, och om det kan bevisa att varje gren börjar med ett annat starttecken, genererar den en switch-instruktion över det första tecknet och undviker att mata ut någon bakåtspårningskod för den växlingen.

Här är ett lite mer komplicerat exempel på det. Alternationer analyseras mer för att avgöra om det är möjligt att omstrukturera dem på ett sätt som gör dem enklare optimerade av backtracking-motorerna och som leder till enklare källgenererad kod. En sådan optimering stöder extrahering av vanliga prefix från grenar, och om växlingen är atomisk så att ordningen inte spelar någon roll, ordna om grenar för att möjliggöra mer sådan extrahering. Du kan se effekten av det för följande veckodagsmönster Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday, som ger en matchande funktion som den här:

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

Observera hur Thursday du har ordnat om till strax efter Tuesday, och hur du för både/TuesdayThursday paret och Saturday/Sunday paret får flera nivåer av växlar. I det extrema fallet, om du skulle skapa en lång växling av många olika ord, skulle källgeneratorn i slutändan generera den logiska motsvarigheten till en trie^1, läsa varje tecken och switch'ing till grenen för att hantera resten av ordet. Detta är ett mycket effektivt sätt att matcha ord, och det är vad källgeneratorn gör här.

Samtidigt har källgeneratorn andra problem att hantera som helt enkelt inte finns när du matar ut till IL direkt. Om du tittar tillbaka på några kodexempel kan du se några klammerparenteser som är något konstigt kommenterade. Det är inget misstag. Källgeneratorn inser att om dessa klammerparenteser inte kommenterades ut förlitar sig backtrackingens struktur på att hoppa från utanför omfånget till en etikett som definierats i det omfånget. en sådan etikett skulle inte vara synlig för en goto sådan och koden skulle misslyckas med att kompilera. Källgeneratorn måste därför undvika att det finns ett omfång i vägen. I vissa fall kommenterar den bara ut omfånget som det gjordes här. I andra fall där det inte är möjligt kan det ibland undvika konstruktioner som kräver omfång (till exempel ett block med flera instruktioner if ) om det skulle vara problematiskt.

Källgeneratorn hanterar allt RegexCompiler som hanteras, med ett undantag. Precis som med hanteringen RegexOptions.IgnoreCaseanvänder implementeringarna nu en höljetabell för att generera uppsättningar vid byggtid, och hur IgnoreCase matchning av backreference måste konsultera den höljetabellen. Tabellen är intern för System.Text.RegularExpressions.dll, och för tillfället har åtminstone koden som är extern till den sammansättningen (inklusive kod som genereras av källgeneratorn) inte åtkomst till den. Det gör hantering IgnoreCase av backreferences till en utmaning i källgeneratorn och de stöds inte. Det här är den enda konstruktion som inte stöds av källgeneratorn som stöds av RegexCompiler. Om du försöker använda ett mönster som har något av dessa (vilket är ovanligt) genererar källgeneratorn inte någon anpassad implementering och återgår i stället till att cachelagra en vanlig Regex instans:

Regex som inte stöds cachelagras fortfarande

Dessutom stöder varken RegexCompiler eller källgeneratorn den nya RegexOptions.NonBacktracking. Om du anger RegexOptions.Compiled | RegexOptions.NonBacktrackingCompiled ignoreras flaggan, och om du anger NonBacktracking för källgeneratorn återgår den på samma sätt till att cachelagra en vanlig Regex instans.

När du ska använda detta

Den allmänna vägledningen är om du kan använda källgeneratorn, använd den. Om du använder Regex i dag i C# med argument som är kända vid kompileringstiden, och särskilt om du redan använder RegexOptions.Compiled (eftersom regex har identifierats som en frekvent punkt som skulle dra nytta av snabbare dataflöde), bör du föredra att använda källgeneratorn. Källgeneratorn ger din regex följande fördelar:

  • Alla dataflödesfördelar med RegexOptions.Compiled.
  • Startfördelarna med att inte behöva utföra all regex-parsning, analys och kompilering vid körning.
  • Alternativet att använda kompilering i förväg med koden som genereras för regex.
  • Bättre debuggability och förståelse för regex.
  • Möjligheten att minska storleken på din trimmade app genom att trimma ut stora mängder kod som är associerad med (och potentiellt till och med RegexCompiler reflektion genererar sig själv).

När det används med ett alternativ som RegexOptions.NonBacktracking som källgeneratorn inte kan generera en anpassad implementering för, genererar den fortfarande cachelagrings- och XML-kommentarer som beskriver implementeringen, vilket gör den värdefull. Den huvudsakliga nackdelen med källgeneratorn är att den genererar ytterligare kod i din sammansättning, så det finns potential för ökad storlek. Ju fler regexes i din app och ju större de är, desto mer kod genereras för dem. I vissa situationer kan det RegexOptions.Compiled vara onödigt, så det kan också vara källgeneratorn. Om du till exempel har ett regex som bara behövs sällan och för vilket dataflöde inte spelar någon roll, kan det vara mer fördelaktigt att bara förlita sig på tolken för den sporadiska användningen.

Viktigt!

.NET 7 innehåller en analysator som identifierar användningen av Regex som kan konverteras till källgeneratorn och en korrigering som utför konverteringen åt dig:

RegexGenerator analyzer och fixer

Se även