Générateurs de sources d’expressions régulières .NET

Une expression régulière, ou regex, est une chaîne qui permet à un développeur d’exprimer un modèle recherché, ce qui en fait un moyen très courant de rechercher du texte et d’extraire les résultats en tant que sous-ensemble de la chaîne recherchée. Dans .NET, l’espace de noms System.Text.RegularExpressions est utilisé pour définir des instances et méthodes statiques Regex, et trouver une correspondance avec des modèles définis par l’utilisateur. Dans cet article, vous allez apprendre à utiliser la génération de sources pour générer des instances Regex afin d’optimiser les performances.

Notes

Si possible, utilisez des expressions régulières générées par la source au lieu de compiler des expressions régulières avec l’option RegexOptions.Compiled. Grâce à la génération de source, votre application peut démarrer plus vite, s’exécuter plus rapidement et mieux se prêter au découpage. Pour savoir quand la génération de source est possible, consultez Quand l’utiliser.

Expressions régulières compilées

Lorsque vous écrivez new Regex("somepattern"), certaines choses se produisent. Le modèle spécifié est analysé, à la fois pour garantir la validité du modèle et pour le transformer en une arborescence interne qui représente le regex analysé. L’arborescence est ensuite optimisée de différentes manières, transformant ainsi le modèle en une variation fonctionnellement équivalente qui peut être exécutée plus efficacement. L’arborescence est écrite sous une forme qui peut être interprétée comme une série d’opcodes et d’opérandes qui fournissent des instructions au moteur de l’interpréteur regex sur la façon d’effectuer la correspondance. Lorsqu’une correspondance est effectuée, l’interpréteur parcourt tout simplement ces instructions et les traite par rapport au texte d’entrée. Lors de l’instanciation d’une nouvelle instance Regex ou de l’appel de l’une des méthodes statiques sur Regex, l’interpréteur est le moteur par défaut utilisé.

Lorsque vous spécifiez RegexOptions.Compiled, le même travail au moment de la construction est effectué. Les instructions résultantes sont davantage transformées par le compilateur basé sur l’émission de réflexion en instructions en langage intermédiaire qui sont écrites en quelques DynamicMethod. Lorsqu’une correspondance a été effectuée, ces DynamicMethod sont appelées. Ce langage intermédiaire fonctionnerait fondamentalement comme l’interprète, à l’exception qu’il serait spécialisé pour le modèle exact traité. Par exemple, si le modèle contenait [ac], l’interpréteur verrait un opcode indiquant « faire correspondre le caractère d’entrée à la position actuelle par rapport au jeu spécifié dans cette description de jeu », tandis que le langage intermédiaire compilé contiendrait du code indiquant « faire correspondre le caractère d’entrée à la position actuelle par rapport à 'a' ou 'c' ». Cette casse spéciale et la possibilité d’effectuer des optimisations en fonction de la connaissance du modèle sont quelques-unes des principales raisons pour lesquelles spécifier RegexOptions.Compiled génère un débit avec une correspondance beaucoup plus rapide que l’interpréteur.

Il existe plusieurs inconvénients à RegexOptions.Compiled. Le plus impactant est qu’il entraîne beaucoup plus de coûts de construction que l’utilisation de l’interpréteur. Non seulement les coûts payés sont les mêmes que pour l’interpréteur, mais il doit ensuite compiler cette arborescence RegexNode résultante et générer des opcodes/opérandes en langage intermédiaire, ce qui ajoute des dépenses non négligeables. Le langage intermédiaire généré doit en outre être compilé par JIT lors de la première utilisation, ce qui entraîne encore plus de dépenses au démarrage. RegexOptions.Compiled représente un compromis fondamental entre les surcharges sur la première utilisation et les surcharges sur chaque utilisation ultérieure. L’utilisation de System.Reflection.Emit empêche également l’utilisation de RegexOptions.Compiled dans certains environnements ; certains systèmes d’exploitation ne permettent pas l’exécution du code généré dynamiquement, et sur ces systèmes, Compiled devient une opération non efficace.

Génération de la source

.NET 7 a introduit un nouveau générateur de source RegexGenerator. Lorsque le compilateur C# a été réécrit en tant que compilateur C# « Roslyn », il exposait des modèles objet pour l’ensemble du pipeline de compilation, ainsi que des analyseurs. Plus récemment, Roslyn activait des générateurs de sources. Tout comme un analyseur, un générateur de sources est un composant qui se connecte au compilateur et reçoit les mêmes informations qu’un analyseur, mais en plus de pouvoir émettre des diagnostics, il peut enrichir l’unité de compilation avec du code source supplémentaire. Le Kit de développement logiciel (SDK) .NET 7+ inclut un nouveau générateur de source qui reconnaît le nouveau GeneratedRegexAttribute sur une méthode partielle qui retourne Regex. Le générateur de sources fournit une implémentation de cette méthode qui implémente toute la logique pour Regex. Par exemple, vous avez peut-être écrit du code comme suit :

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

Vous pouvez maintenant réécrire le code précédent comme suit :

[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’implémentation générée de AbcOrDefGeneratedRegex() met en cache de la même façon une instance Regex singleton, de sorte qu’aucune mise en cache supplémentaire n’est nécessaire pour consommer du code.

Conseil

L’indicateur RegexOptions.Compiled est ignoré par le générateur source, ce qui le rend inutile dans la version générée par la source.

L’image suivante est une capture d’écran de l’instance mise en cache générée par la source, internal dans la sous-classe Regex émise par le générateur de source :

Champ statique regex mis en cache

Mais comme on peut le voir, il ne s’agit pas seulement de taper new Regex(...). Au lieu de cela, le générateur de sources émet en tant que code C# une implémentation dérivée de Regex personnalisée avec une logique similaire à ce que RegexOptions.Compiled émet en langage intermédiaire. Vous bénéficiez de tous les avantages (voire plus) en matière de performances de débit de RegexOptions.Compiled et des avantages de démarrage de Regex.CompileToAssembly, mais sans la complexité de CompileToAssembly. La source émise fait partie de votre projet, ce qui signifie qu’elle est facilement visible et débogable.

Débogage via du code Regex généré par la source

Conseil

Dans Visual Studio, cliquez avec le bouton droit sur votre déclaration de méthode partielle et sélectionnez Atteindre la définition. Vous pouvez également sélectionner le nœud de projet dans Explorateur de solutions, puis développer Dépendances>Analyzers>System.Text.RegularExpressions.Generator>System.Text.RegularExpressions.Generator.RegexGenerator>RegexGenerator.g.cs pour voir le code C# généré à partir de ce générateur d’expressions régulières.

Vous pouvez définir des points d’arrêt dans celui-ci, vous pouvez effectuer un pas à pas et vous pouvez l’utiliser comme outil d’apprentissage pour comprendre la façon exacte dont le moteur regex traite votre modèle avec votre entrée. Le générateur génère même des commentaires XML (triple barre oblique) pour rendre l’expression compréhensible en un coup d’œil et à l’endroit où elle est utilisée.

Commentaires XML générés décrivant regex

À l’intérieur des fichiers générés par la source

Avec .NET 7, le générateur de sources et RegexCompiler ont été presque entièrement réécrits, modifiant fondamentalement la structure du code généré. Cette approche a été étendue pour gérer toutes les constructions (avec une mise en garde). Par ailleurs, tant RegexCompiler que le générateur de sources continuent de mapper la plupart du temps de un-à-un (1:1) l’un avec l’autre, en suivant la nouvelle approche. Considérez la sortie du générateur de sources pour l’une des fonctions principales de l’expression (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’objectif du code généré par la source est d’être compréhensible, avec une structure facile à suivre, avec des commentaires expliquant ce qui est fait à chaque étape, et en général avec le code émis selon le principe directeur selon lequel le générateur doit émettre du code comme si un humain l’avait écrit. Même lorsque le retour sur trace est impliqué, la structure du retour sur trace fait partie de la structure du code, plutôt que de s’appuyer sur une pile pour indiquer où aller ensuite. Par exemple, voici le code de la même fonction de mise en correspondance générée lorsque l’expression est [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;
}

Vous pouvez voir la structure du retour sur trace dans le code, avec une étiquette CharLoopBacktrack émise pour l’endroit où effectuer un retour en arrière et une instruction goto utilisée pour accéder à cet emplacement lorsqu’une partie ultérieure du regex échoue.

Si vous examinez le code implémentant RegexCompiler et le générateur de sources, ils seront extrêmement similaires : méthodes nommées de la même façon, structure d’appel similaire et même commentaires similaires tout au long de l’implémentation. Ils aboutissent en grande partie à un code identique, bien que l’un soit en langage intermédiaire et l’autre en C#. Bien sûr, le compilateur C# est alors responsable de la traduction de C# en langage intermédiaire, de sorte que le langage intermédiaire résultant dans les deux cas ne sera probablement pas identique. Le générateur de sources s’appuie sur cela dans différents cas, en tirant parti du fait que le compilateur C# optimisera davantage différentes constructions C#. Il existe quelques éléments spécifiques qui font que le générateur de sources produira donc plus de code de mise en correspondance optimisé que ne le fait RegexCompiler. Par exemple, dans l’un des exemples précédents, vous pouvez voir le générateur de sources qui émet une instruction switch, avec une branche pour 'a' et une autre branche pour 'b'. Étant donné que le compilateur C# est très bon pour optimiser les instructions switch, avec plusieurs stratégies à sa disposition pour savoir comment le faire efficacement, le générateur de sources a une optimisation spéciale que RegexCompiler ne fait pas. Pour les alternances, le générateur de sources examine toutes les branches et, s’il peut prouver que chaque branche commence par un caractère de départ différent, il émet une instruction switch sur ce premier caractère et évite de placer un code de retour sur trace pour cette alternance.

Voici un exemple un peu plus complexe de cela. Les alternances sont analysées de façon plus poussée afin de déterminer s’il est possible de les refactoriser de manière à les rendre plus facilement optimisables par les moteurs de retour sur trace, ce qui simplifiera le code généré par la source. L’une de ces optimisations prend en charge l’extraction de préfixes communs à partir de branches, et si l’alternance est atomique, de telle sorte que le classement n’a pas d’importance, réorganiser les branches pour permettre une extraction plus importante de ce type. Vous pouvez voir l’impact de cela pour le modèle de jour de la semaine suivant Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday, qui produit une fonction de mise en correspondance comme ceci :

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

Notez comment Thursday a été réorganisé pour être situé juste après Tuesday, et comment pour la paire Tuesday/Thursday et la paire Saturday/Sunday, vous vous retrouvez avec plusieurs niveaux de commutateurs. À l’extrême, si vous deviez créer une longue alternance de nombreux mots différents, le générateur de sources finirait par émettre l’équivalent logique d’un trie^1, en lisant chaque caractère et en effectuant un switch vers la branche pour gérer le reste du mot. Il s’agit d’un moyen très efficace de faire correspondre les mots, et c’est ce que fait le générateur de sources ici.

En même temps, le générateur de sources a d’autres problèmes à résoudre qui n’existent tout simplement pas lors de la génération d’une sortie en langage intermédiaire directement. Si vous regardez quelques exemples de code plus haut, vous pouvez voir des accolades aux commentaires quelque peu étranges. Ce n’est pas une erreur. Le générateur de sources reconnaît que, si ces accolades n’ont pas été commentées, la structure du retour sur trace repose sur le saut de l’extérieur de l’étendue vers une étiquette définie à l’intérieur de cette étendue ; une telle étiquette ne serait pas visible pour un tel goto et le code ne serait pas compilé. Par conséquent, le générateur de sources doit éviter qu’il y ait une étendue au beau milieu. Dans certains cas, il commente simplement l’étendue comme cela a été fait ici. Dans d’autres cas où cela n’est pas possible, il peut parfois éviter les constructions qui nécessitent des étendues (comme un bloc à plusieurs instructions if) si cela est problématique.

Le générateur de sources gère tout ce que RegexCompiler gère, à une exception près. Comme pour la gestion de RegexOptions.IgnoreCase, les implémentations utilisent à présent une table de casse pour générer des jeux au moment de la construction, et la façon dont la mise en correspondance de référence arrière IgnoreCase doit consulter cette table de casse. Cette table est interne à System.Text.RegularExpressions.dll, et pour l’instant, au moins, le code externe à cet assembly (y compris le code émis par le générateur de sources) n’y a pas accès. Cela rend la gestion des références arrières IgnoreCase complexe dans le générateur de sources et elles ne sont pas prises en charge. Il s’agit de la seule construction non prise en charge par le générateur de sources pris en charge par RegexCompiler. Si vous essayez d’utiliser un modèle contenant l’un de ces modèles (ce qui est rare), le générateur de sources n’émettra pas d’implémentation personnalisée et reviendra à la mise en cache d’une instance Regex régulière :

Regex non pris en charge toujours en cache

En outre, ni le générateur de sources ni RegexCompiler ne prend en charge le nouveau RegexOptions.NonBacktracking. Si vous spécifiez RegexOptions.Compiled | RegexOptions.NonBacktracking, l’indicateur Compiled sera simplement ignoré, et si vous spécifiez NonBacktracking dans le générateur de sources, il revient également à la mise en cache d’une instance régulière Regex.

Quand utiliser cette fonctionnalité ?

Si vous pouvez utiliser le générateur de sources, faites-le. Si vous utilisez Regex aujourd’hui en C# avec des arguments connus au moment de la compilation, et surtout si vous utilisez déjà RegexOptions.Compiled (car le regex a été identifié comme une zone réactive qui bénéficierait d’un débit plus rapide), vous devez préférer utiliser le générateur de sources. Le générateur de sources offre à votre regex les avantages suivants :

  • Tous les avantages en matière de débit de RegexOptions.Compiled.
  • Les avantages au démarrage (à savoir ne pas avoir à effectuer l’analyse) et la compilation regex au moment de l’exécution.
  • Option d’utilisation de la compilation Ahead-of-time avec le code généré pour le regex.
  • Débogage optimisé et meilleure compréhension du regex.
  • La possibilité de réduire la taille de votre application rognée en réduisant les grandes étendues de code associées à RegexCompiler (et potentiellement même l’émission de réflexion elle-même).

Lorsqu’il est utilisé avec une option comme RegexOptions.NonBacktracking pour laquelle le générateur de sources ne peut pas générer d’implémentation personnalisée, il émet toujours des commentaires de mise en cache et XML qui décrivent l’implémentation, ce qui a de la valeur. Le principal inconvénient du générateur de sources est qu’il émet du code supplémentaire dans votre assembly, ce qui permet d’augmenter la taille. Plus il y a d’expressions régulières dans votre application et plus elles sont volumineuses, plus le code émis est volumineux. Dans certaines situations, le générateur de sources peut être inutile tout comme RegexOptions.Compiled. Par exemple, si vous avez un regex qui n’est nécessaire que rarement et pour lequel le débit n’a pas d’importance, il peut être plus utile de s’appuyer simplement sur l’interpréteur pour cette utilisation sporadique.

Important

.NET 7 inclut un analyseur qui identifie l’utilisation de Regex qui peut être convertie en générateur de sources, et un correcteur qui effectue la conversion pour vous :

Analyseur et fixateur RegexGenerator

Voir aussi