Generatori dell'origine di espressioni regolari .NET

Un'espressione regolare, o regex, è una stringa che consente a uno sviluppatore di esprimere un criterio di ricerca, rendendolo un modo molto comune per cercare testo ed estrarre i risultati come subset dalla stringa ricercata. In .NET lo System.Text.RegularExpressions spazio dei nomi viene usato per definire Regex istanze e metodi statici e corrispondere ai modelli definiti dall'utente. In questo articolo si apprenderà come usare la generazione di origine per generare Regex istanze per ottimizzare le prestazioni.

Nota

Se possibile, usare le espressioni regolari generate dall'origine anziché compilare espressioni regolari usando l'opzione RegexOptions.Compiled . La generazione di origine può aiutare l'app ad avviarsi ed eseguire più velocemente ed essere più compatibile con il trimming. Per informazioni su quando è possibile generare l'origine, vedere Quando usarla.

Espressioni regolari compilate

Quando si scrive new Regex("somepattern"), si verificano alcune cose. Il modello specificato viene analizzato, sia per garantire la validità del modello che per trasformarlo in un albero interno che rappresenta l'espressione regolare analizzata. L'albero viene quindi ottimizzato in vari modi, trasformando il modello in una variante funzionalmente equivalente che può essere eseguita in modo più efficiente. L'albero viene scritto in un modulo che può essere interpretato come una serie di opcode e operandi che forniscono istruzioni al motore dell'interprete regex su come trovare la corrispondenza. Quando viene eseguita una corrispondenza, l'interprete scorre semplicemente le istruzioni, elaborandole sul testo di input. Quando si crea un'istanza di una nuova Regex istanza o si chiama uno dei metodi statici in Regex, l'interprete è il motore predefinito usato.

Quando si specifica RegexOptions.Compiled, verranno eseguiti tutti gli stessi lavori in fase di costruzione. Le istruzioni risultanti verranno trasformate ulteriormente dal compilatore basato su reflection-emit in istruzioni IL che verrebbero scritte in poche DynamicMethod. Quando è stata eseguita una corrispondenza, questi DynamicMethodvengono richiamati. Questo processo di bilanciamento del carico interno esegue essenzialmente esattamente ciò che l'interprete farebbe, tranne che per il modello esatto in fase di elaborazione. Se, ad esempio, il modello conteneva [ac], l'interprete visualizzerebbe un codice operativo che ha detto "corrisponde al carattere di input nella posizione corrente rispetto al set specificato in questa descrizione del set", mentre il codice compilato conterrà codice che ha detto in modo efficace, "associare il carattere di input alla posizione 'a' corrente rispetto a o 'c'". Questa combinazione speciale di maiuscole e minuscole e la possibilità di eseguire ottimizzazioni in base alla conoscenza del modello sono alcuni dei motivi principali per specificare RegexOptions.Compiled la velocità effettiva molto più veloce rispetto all'interprete.

Esistono diversi aspetti negativi di RegexOptions.Compiled. L'impatto maggiore è che comporta un costo di costruzione molto maggiore rispetto all'uso dell'interprete. Non solo sono tutti gli stessi costi pagati per l'interprete, ma deve quindi compilare l'albero risultante RegexNode e generare opcodes/operandi in IL, che aggiunge spese non semplici. L'IL generato deve essere compilato ulteriormente in fase di compilazione JIT al primo utilizzo, con conseguente ulteriore spesa all'avvio. RegexOptions.Compiled rappresenta un compromesso fondamentale tra i sovraccarichi per il primo utilizzo e i sovraccarichi per ogni utilizzo successivo. L'uso di impedisce anche l'uso di System.Reflection.EmitRegexOptions.Compiled in determinati ambienti. Alcuni sistemi operativi non consentono l'esecuzione di codice generato dinamicamente e in tali sistemi Compiled diventano no-op.

Generazione dell'origine

.NET 7 ha introdotto un nuovo RegexGenerator generatore di origine. Quando il compilatore C# è stato riscritto come compilatore C# "Roslyn", ha esposto i modelli a oggetti per l'intera pipeline di compilazione, nonché gli analizzatori. Più di recente, Roslyn ha abilitato generatori di origine. Proprio come un analizzatore, un generatore di origine è un componente che collega il compilatore e passa tutte le stesse informazioni di un analizzatore, ma oltre a poter generare diagnostica, può anche aumentare l'unità di compilazione con codice sorgente aggiuntivo. .NET 7+ SDK include un nuovo generatore di origine che riconosce il nuovo GeneratedRegexAttribute in un metodo parziale che restituisce Regex. Il generatore di origine fornisce un'implementazione di tale metodo che implementa tutta la logica per .Regex Ad esempio, è possibile che sia stato scritto codice simile al seguente:

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

È ora possibile riscrivere il codice precedente come segue:

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

L'implementazione generata di AbcOrDefGeneratedRegex() memorizza nella cache un'istanza singleton Regex in modo analogo, pertanto non è necessaria alcuna memorizzazione nella cache aggiuntiva per utilizzare il codice.

Suggerimento

Il RegexOptions.Compiled flag viene ignorato dal generatore di origine, rendendolo quindi non più necessario nella versione generata dall'origine.

L'immagine seguente è un'acquisizione dello schermo dell'istanza memorizzata nella cache generata dall'origine, internal nella Regex sottoclasse generata dal generatore di origine:

Campo statico regex memorizzato nella cache

Ma come si può vedere, non è solo fare new Regex(...). Il generatore di origine genera invece come codice C# un'implementazione personalizzata Regexderivata con logica simile a quella RegexOptions.Compiled che emette in IL. Si ottengono tutti i vantaggi delle prestazioni della velocità effettiva di RegexOptions.Compiled (più, in effetti) e i vantaggi di avvio di Regex.CompileToAssembly, ma senza la complessità di CompileToAssembly. L'origine generata fa parte del progetto, il che significa che è anche facilmente visualizzabile e di cui è possibile eseguire il debug.

Debug tramite codice Regex generato dall'origine

Suggerimento

In Visual Studio fare clic con il pulsante destro del mouse sulla dichiarazione di metodo parziale e scegliere Vai a definizione. In alternativa, selezionare il nodo del progetto in Esplora soluzioni, quindi espandere Dependencies>Analyzers>System.Text.RegularExpressions.Generator.Text.RegularExpressions.Generator.RegexGenerator>>RegexGenerator.g.cs per visualizzare il codice C# generato da questo generatore regex.

È possibile impostare punti di interruzione in esso, è possibile eseguirne il passaggio e usarlo come strumento di apprendimento per comprendere esattamente come il motore regex sta elaborando il modello con l'input. Il generatore genera anche commenti a barre triple (XML) per rendere l'espressione comprensibile a colpo d'occhio e dove viene usata.

Commenti XML generati che descrivono l'espressione regolare

All'interno dei file generati dall'origine

Con .NET 7, sia il generatore di origine che RegexCompiler sono stati quasi completamente riscritti, modificando fondamentalmente la struttura del codice generato. Questo approccio è stato esteso per gestire tutti i costrutti (con un'avvertenza) e RegexCompiler sia che il generatore di origine e mappano per lo più 1:1 tra loro, seguendo il nuovo approccio. Si consideri l'output del generatore di origine per una delle funzioni primarie dell'espressione (a|bc)d :

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

L'obiettivo del codice generato dall'origine è essere comprensibile, con una struttura facile da seguire, con commenti che spiegano cosa viene fatto in ogni passaggio e in generale con il codice generato sotto il principio guida che il generatore deve generare codice come se fosse stato scritto da un essere umano. Anche quando è coinvolto il backtracking, la struttura del backtracking diventa parte della struttura del codice, anziché basarsi su uno stack per indicare dove saltare. Ad esempio, di seguito è riportato il codice per la stessa funzione di corrispondenza generata quando l'espressione è [ab]*[bc]:

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 charloop_starting_pos = 0, charloop_ending_pos = 0;
    ReadOnlySpan<char> slice = inputSpan.Slice(pos);

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

        int iteration = slice.IndexOfAnyExcept('a', 'b');
        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('b', 'c')) < 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 [bc].
    if (slice.IsEmpty || !char.IsBetween(slice[0], 'b', 'c'))
    {
        goto CharLoopBacktrack;
    }

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

È possibile visualizzare la struttura del backtracking nel codice, con un'etichetta CharLoopBacktrack generata per dove eseguire il backtracking in e un goto oggetto usato per passare a tale posizione quando una parte successiva dell'espressione regolare ha esito negativo.

Se si esamina il codice che implementa RegexCompiler e il generatore di origine, verranno visualizzati in modo estremamente simile: metodi denominati in modo simile, struttura di chiamata simile e anche commenti simili in tutta l'implementazione. Nella maggior parte dei casi, generano codice identico, anche se uno in IL e uno in C#. Naturalmente, il compilatore C# è quindi responsabile della conversione di C# in IL, quindi il risultato IL in entrambi i casi probabilmente non sarà identico. Il generatore di origine si basa su questo in vari casi, sfruttando il fatto che il compilatore C# ottimizzerà ulteriormente vari costrutti C#. Esistono alcuni aspetti specifici che il generatore di origine produrrà quindi codice di corrispondenza più ottimizzato rispetto a .RegexCompiler Ad esempio, in uno degli esempi precedenti, è possibile visualizzare il generatore di origine che emette un'istruzione switch, con un ramo per 'a' e un altro ramo per 'b'. Poiché il compilatore C# è molto utile per ottimizzare le istruzioni switch, con più strategie a disposizione per come farlo in modo efficiente, il generatore di origine ha un'ottimizzazione speciale che RegexCompiler non lo è. Per le alternanze, il generatore di origine esamina tutti i rami e, se può dimostrare che ogni ramo inizia con un carattere iniziale diverso, genererà un'istruzione switch su quel primo carattere ed evitare di restituire qualsiasi codice di backtracking per tale alternanza.

Ecco un esempio leggermente più complicato di questo. Le alternanze vengono analizzate in modo più approfondito per determinare se è possibile effettuare il refactoring in modo da renderle più facilmente ottimizzate dai motori di backtracking e ciò comporterà un codice più semplice generato dall'origine. Un'ottimizzazione di questo tipo supporta l'estrazione di prefissi comuni dai rami e se l'alternanza è atomica, in modo che l'ordinamento non sia importante, riordinare i rami per consentire un'estrazione più tale. È possibile visualizzare l'impatto di tale comportamento per il modello Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sundaydi giorno feriale seguente, che produce una funzione corrispondente simile alla seguente:

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

Si noti come Thursday è stato riordinato per essere subito dopo Tuesdaye come sia per la/ThursdayTuesdaycoppia che per la Saturday/Sunday coppia, si finisce con più livelli di commutatori. Nell'estremo, se si dovesse creare un'alternanza lunga di molte parole diverse, il generatore di origine finirebbe per emettere l'equivalente logico di un trie^1, leggendo ogni carattere e switch'ing al ramo per gestire il resto della parola. Si tratta di un modo molto efficiente per trovare le parole, ed è ciò che il generatore di origine sta facendo qui.

Allo stesso tempo, il generatore di origine presenta altri problemi per affrontare che semplicemente non esistono quando si esegue direttamente l'output in IL. Se guardi un paio di esempi di codice indietro, puoi vedere alcune parentesi graffe un po 'stranamente commentate. Non è un errore. Il generatore di origine riconosce che, se tali parentesi graffe non sono state commentate, la struttura del backtracking si basa sul passaggio dall'esterno dell'ambito a un'etichetta definita all'interno di tale ambito; tale etichetta non sarebbe visibile a tale e goto il codice non sarebbe in grado di compilare. Pertanto, il generatore di origine deve evitare che vi sia un ambito nel modo. In alcuni casi, è sufficiente impostare come commento l'ambito come è stato fatto qui. In altri casi in cui non è possibile, a volte può evitare costrutti che richiedono ambiti (ad esempio un blocco con più istruzioni if ) in caso di problemi.

Il generatore di origine gestisce tutti gli RegexCompiler handle, con un'unica eccezione. Come per la gestione di RegexOptions.IgnoreCase, le implementazioni ora usano una tabella di maiuscole e minuscole per generare set in fase di costruzione e il IgnoreCase modo in cui la corrispondenza di backreference deve consultare tale tabella di maiuscole e minuscole. Tale tabella è interna a System.Text.RegularExpressions.dlle per il momento, almeno, il codice esterno a tale assembly (incluso il codice generato dal generatore di origine) non ha accesso. Ciò rende la gestione IgnoreCase dei backreference una sfida nel generatore di origine e non sono supportati. Si tratta di un costrutto non supportato dal generatore di origine supportato da RegexCompiler. Se si tenta di usare un modello con uno di questi (che è raro), il generatore di origine non genererà un'implementazione personalizzata e eseguirà invece il fallback alla memorizzazione nella cache di un'istanza regolare Regex :

Regex non supportato ancora memorizzato nella cache

Inoltre, né RegexCompiler il generatore di origine supporta il nuovo RegexOptions.NonBacktracking. Se si specifica RegexOptions.Compiled | RegexOptions.NonBacktracking, il Compiled flag verrà semplicemente ignorato e, se si specifica NonBacktracking al generatore di origine, verrà eseguito il fallback alla memorizzazione nella cache di un'istanza regolare Regex .

Quando usarlo

Le indicazioni generali sono se è possibile usare il generatore di origine, usarlo. Se si usa Regex oggi in C# con argomenti noti in fase di compilazione e soprattutto se si usa RegexOptions.Compiled già (perché l'espressione regolare è stata identificata come un punto critico che trarrebbe vantaggio dalla velocità effettiva più rapida), è consigliabile usare il generatore di origine. Il generatore di origine offrirà i vantaggi seguenti:

  • Tutti i vantaggi della velocità effettiva di RegexOptions.Compiled.
  • I vantaggi di avvio di non dover eseguire tutte le analisi, l'analisi e la compilazione regex in fase di esecuzione.
  • Opzione di utilizzo della compilazione anticipata con il codice generato per l'espressione regolare.
  • Migliore debug e comprensione dell'espressione regolare.
  • La possibilità di ridurre le dimensioni dell'app tagliata tagliando grandi swath di codice associato RegexCompiler (e potenzialmente anche la reflection genera se stessa).

Se usato con un'opzione come RegexOptions.NonBacktracking per cui il generatore di origine non può generare un'implementazione personalizzata, genera comunque commenti di memorizzazione nella cache e XML che descrivono l'implementazione, rendendolo utile. Lo svantaggio principale del generatore di origine è che genera codice aggiuntivo nell'assembly, quindi c'è il potenziale per aumentare le dimensioni. Più regexes nell'app e maggiori sono, più codice verrà generato per loro. In alcune situazioni, proprio come RegexOptions.Compiled potrebbe non essere necessario, quindi troppo potrebbe essere il generatore di origine. Ad esempio, se si dispone di un'espressione regolare necessaria solo raramente e per cui la velocità effettiva non è rilevante, potrebbe essere più utile affidarsi solo all'interprete per l'utilizzo sporadico.

Importante

.NET 7 include un analizzatore che identifica l'uso di che può essere convertito nel generatore di Regex origine e un fixer che esegue automaticamente la conversione:

Analizzatore RegexGenerator e fixer

Vedi anche