Geradores de origem de expressão regular do .NET

Uma expressão regular, ou regex, é uma cadeia de caracteres que permite que um desenvolvedor expresse um padrão que está sendo pesquisado, tornando-o uma maneira muito comum de pesquisar texto e extrair resultados como um subconjunto da cadeia de caracteres pesquisada. No .NET, o namespace System.Text.RegularExpressions é usado para definir instâncias e métodos estáticos Regex e corresponder a padrões definidos pelo usuário. Neste artigo, você aprenderá a usar a geração de origem para gerar instâncias Regex para otimizar o desempenho.

Observação

Sempre que possível, use expressões regulares geradas pela origem em vez de compilar expressões regulares usando a opção RegexOptions.Compiled. A geração de origem pode ajudar seu aplicativo a iniciar com mais rapidez, ser executado com mais agilidade e ser mais completo. Para saber quando a geração de origem é possível, confira Quando usá-la.

Expressões regulares compiladas

Quando você escreve new Regex("somepattern"), algumas coisas acontecem. O padrão especificado é analisado, tanto para garantir a validade do padrão quanto para transformá-lo em uma árvore interna que representa o regex analisado. Em seguida, a árvore é otimizada de várias maneiras, transformando o padrão em uma variação funcionalmente equivalente que pode ser executada com mais eficiência. A árvore é gravada em um formulário que pode ser interpretado como uma série de opcodes e operandos que fornecem instruções ao mecanismo do interpretador regex sobre como fazer a correspondência. Quando uma correspondência é executada, o interpretador simplesmente percorre essas instruções, processando-as em relação ao texto de entrada. Ao instanciar uma nova instância Regex ou chamar um dos métodos estáticos em Regex, o interpretador é o mecanismo padrão empregado.

Quando você especificar RegexOptions.Compiled, todas as mesmas obras em tempo de construção serão executadas. As instruções resultantes seriam transformadas ainda mais pelo compilador baseado em emissão de reflexão em instruções IL que seriam gravadas em alguns DynamicMethods. Quando uma correspondência era executada, esses DynamicMethods seriam invocados. De forma essencial, essa IL faria exatamente o que o interpretador faria, exceto pela especialização para o padrão exato que está sendo processado. Por exemplo, se o padrão contivesse [ac], o interpretador veria um opcode que diria "combinar o caractere de entrada na posição atual com o conjunto especificado nesta descrição do conjunto", enquanto o IL compilado conteria o código que efetivamente diria "combinar com o caractere de entrada na posição atual contra 'a' ou 'c'". Esse caso especial e a capacidade de realizar otimizações com base no conhecimento do padrão são algumas das principais razões para especificar RegexOptions.Compiled, resultando em uma taxa de transferência de correspondência muito mais rápida do que o interpretador.

Há várias desvantagens em RegexOptions.Compiled. A mais impactante é que ela incorre em muito mais custo de construção do que usar o interpretador. Não apenas todos os mesmos custos são pagos pelo interpretador, mas ele também precisa compilar a árvore RegexNode resultante e os opcodes/operandos gerados no IL, o que adiciona despesas não triviais. A IL gerada ainda precisa ser compilada por JIT no primeiro uso, levando a ainda mais despesas na inicialização. RegexOptions.Compiled representa uma compensação fundamental entre sobrecargas no primeiro uso e sobrecargas em cada uso subsequente. O uso de System.Reflection.Emit também inibe o uso de RegexOptions.Compiled em determinados ambientes; alguns sistemas operacionais não permitem que o código gerado dinamicamente seja executado e, nesses sistemas, Compiled se tornará uma operação não operacional.

Geração de origem

O .NET 7 introduziu um novo gerador de origem RegexGenerator. Quando o compilador C# foi reescrito como o compilador C# "Roslyn", ele expôs modelos de objeto para todo o pipeline de compilação, bem como analisadores. Mais recentemente, Roslyn habilitou geradores de origem. Assim como um analisador, um gerador de origem é um componente que se conecta ao compilador e recebe todas as mesmas informações que um analisador, mas além de ser capaz de emitir diagnósticos, ele também pode aumentar a unidade de compilação com código-fonte adicional. O SDK do .NET 7+ inclui um novo gerador de origem que reconhece o novo GeneratedRegexAttribute em um método parcial que retorna Regex. O gerador de origem fornece uma implementação desse método que implementa toda a lógica para o Regex. Por exemplo, você pode ter escrito um código como este:

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

Agora você pode reescrever o código anterior da seguinte maneira:

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

A implementação gerada de AbcOrDefGeneratedRegex() armazena em cache de forma semelhante uma instância singleton Regex, portanto, nenhum cache adicional é necessário para consumir código.

Dica

O sinalizador RegexOptions.Compiled é ignorado pelo gerador de origem, fazendo com que não seja mais necessário na versão gerada pela origem.

A imagem a seguir é uma captura de tela da instância em cache gerada pela origem, internal, para a subclasse Regex que o gerador de origem emite:

Campo estático regex armazenado em cache

Mas como pode ser visto, não é apenas fazer new Regex(...). Em vez disso, o gerador de origem está emitindo como código C# uma implementação derivada de personalizado Regexcom lógica semelhante à que RegexOptions.Compiled emite na IL. Você obtém todos os benefícios de desempenho de taxa de transferência de RegexOptions.Compiled (mais, na verdade) e os benefícios de inicialização de Regex.CompileToAssembly, mas sem a complexidade de CompileToAssembly. A origem emitida faz parte do seu projeto, o que significa que ele também é facilmente acessível e depurável.

Depuração por meio do código Regex gerado pela origem

Dica

No Visual Studio, clique com o botão direito do mouse em sua declaração de método parcial e selecione Ir para Definição. Ou, como alternativa, selecione o nó do projeto no Gerenciador de Soluções e expanda Dependências>Analisadores>System.Text.RegularExpressions.Generator>System.Text.RegularExpressions.Generator.RegexGenerator>RegexGenerator.g.cs para ver o código C# gerado desse gerador regex.

Você pode definir pontos de interrupção nele, percorrê-lo e usá-lo como uma ferramenta de aprendizado para entender exatamente como o mecanismo regex está processando seu padrão com sua entrada. O gerador gera até mesmo comentários de barra tripla (XML) para ajudar a tornar a expressão compreensível rapidamente e onde ela é usada.

Comentários XML gerados que descrevem regex

Dentro dos arquivos gerados pela origem

Com o .NET 7, o gerador de origem e RegexCompiler foram quase inteiramente reescritos, alterando fundamentalmente a estrutura do código gerado. Essa abordagem foi estendida para lidar com todos os constructos (com uma ressalva), e o RegexCompiler e o gerador de origem ainda mapeiam principalmente 1:1 entre si, seguindo a nova abordagem. Considere a saída do gerador de origem para uma das funções primárias da expressão (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();
        }
    }
}

O objetivo do código gerado pela origem é ser compreensível, com uma estrutura fácil de seguir, com comentários explicando o que está sendo feito em cada etapa e, em geral, com o código emitido sob o princípio norteador de que o gerador deve emitir código como se um humano o tivesse escrito. Mesmo quando o rastreamento inverso está envolvido, a estrutura do rastreamento inverso torna-se parte da estrutura do código, em vez de depender de uma pilha para indicar onde saltar em seguida. Por exemplo, aqui está o código para a mesma função de correspondência gerada quando a expressão é [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;
}

Você pode ver a estrutura do rastreamento inverso no código, com um rótulo CharLoopBacktrack emitido para onde retroceder e um goto usado para ir para esse local quando uma parte subsequente do regex falhar.

Se você examinar o código que implementa RegexCompiler e o gerador de origem, eles serão extremamente semelhantes: métodos nomeados da mesma forma, estrutura de chamada semelhante e até mesmo comentários semelhantes em toda a implementação. Na maioria das vezes, eles resultam em código idêntico, embora um em IL e outro em C#. É claro que o compilador C# é então responsável por traduzir o C# para IL, portanto, a IL resultante em ambos os casos provavelmente não será idêntica. O gerador de origem depende disso em vários casos, aproveitando o fato de que o compilador C# otimizará ainda mais vários constructos C#. Há algumas coisas específicas que o gerador de origem produzirá mais código de correspondência otimizado do que o RegexCompiler. Por exemplo, em um dos exemplos anteriores, você pode ver o gerador de origem emitindo uma instrução switch, com um branch para 'a' e outro branch para 'b'. Como o compilador C# é muito bom na otimização de instruções switch, com várias estratégias à sua disposição para fazer isso com eficiência, o gerador de origem tem uma otimização especial que RegexCompiler não tem. Para alternâncias, o gerador de origem examina todos os branches e, se puder provar que cada branch começa com um caractere inicial diferente, emitirá uma instrução switch sobre o primeiro caractere e evitará gerar qualquer código de rastreamento inverso para essa alternância.

Aqui está um exemplo um pouco mais complicado disso. As alternâncias são analisadas mais a fundo para determinar se é possível refatorá-las para serem otimizadas pelos mecanismos de rastreamento inverso com mais facilidade e para que o código gerado pela origem seja mais simples. Uma dessas otimizações dá suporte à extração de prefixos comuns de branches e, se a alternância for atômica de modo que a ordenação não importe, reordene branches para permitir mais extração desse tipo. Você pode ver o impacto disso para o seguinte padrão de dia da semana Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday, que produz uma função correspondente como esta:

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

Observe como Thursday foi reordenado para ser logo após Tuesday, e como para o par Tuesday/Thursday e para o par Saturday/Sunday, você acaba com vários níveis de comutadores. No extremo, se você criasse uma longa alternância de muitas palavras diferentes, o gerador de origem acabaria emitindo o equivalente lógico de um trie^1, lendo cada caractere e switchindo para o branch para lidar com o restante da palavra. Essa é uma maneira muito eficiente de corresponder palavras e é o que o gerador de origem está fazendo aqui.

Ao mesmo tempo, o gerador de origem tem outros problemas para enfrentar que simplesmente não existem ao gerar diretamente para IL. Se você observar alguns exemplos de código de mais uma vez, poderá ver algumas chaves um pouco estranhamente comentadas. Isso não é um erro. O gerador de origem reconhece que, se essas chaves não tiverem sido comentadas, a estrutura do rastreamento inverso dependerá do salto de fora do escopo para um rótulo definido dentro desse escopo; esse rótulo não seria visível para tal goto e o código não seria compilado. Portanto, o gerador de origem precisa evitar que haja um escopo no caminho. Em alguns casos, ele simplesmente comentará o escopo como foi feito aqui. Em outros casos em que isso não é possível, às vezes pode evitar constructos que requerem escopos (como um bloco if de várias instruções) se isso for problemático.

O gerador de origem lida com todos os identificadores RegexCompiler, com uma exceção. Assim como na manipulação de RegexOptions.IgnoreCase, as implementações agora usam uma tabela de revestimento para gerar conjuntos no momento da construção e como a correspondência de referência inversa IgnoreCase precisa consultar essa tabela de revestimento. Essa tabela é interna para System.Text.RegularExpressions.dll e, por enquanto, pelo menos, o código externo a esse assembly (incluindo o código emitido pelo gerador de origem) não tem acesso a ela. Isso torna a manipulação de referências inversas IgnoreCase um desafio no gerador de origem e elas não têm suporte. Este é o único constructo sem suporte pelo gerador de origem com suporte pelo RegexCompiler. Se você tentar usar um padrão que tenha um desses (o que é raro), o gerador de origem não emitirá uma implementação personalizada e, em vez disso, retornará ao cache de uma instância regular Regex:

Regex sem suporte ainda sendo armazenado em cache

Além disso, nem RegexCompiler nem o gerador de origem suportam o novo RegexOptions.NonBacktracking. Se você especificar RegexOptions.Compiled | RegexOptions.NonBacktracking, o sinalizador Compiled será ignorado e, se você especificar NonBacktracking para o gerador de origem, ele retornará de maneira semelhante ao cache de uma instância Regex regular.

Quando usar isso

A orientação geral é se você pode usar o gerador de fonte, use-o. Se você estiver usando Regex hoje em C# com argumentos conhecidos em tempo de compilação e, especialmente, se você já estiver usando RegexOptions.Compiled (porque o regex foi identificado como um ponto de acesso que se beneficiaria de uma taxa de transferência mais rápida), você deve preferir usar o gerador de origem. O gerador de origem dará ao seu regex os seguintes benefícios:

  • Todos os benefícios de taxa de transferência de RegexOptions.Compiled.
  • Os benefícios de inicialização de não precisar fazer toda a análise de regex, análise e compilação em tempo de execução.
  • A opção de usar a compilação antecipada com o código gerado para o regex.
  • Melhor depuração e compreensão do regex.
  • A possibilidade de reduzir o tamanho de seu aplicativo aparado, cortando grandes faixas de código associadas com RegexCompiler (e potencialmente até mesmo a reflexão em si).

Quando usada com uma opção como RegexOptions.NonBacktracking para a qual o gerador de origem não pode gerar uma implementação personalizada, ela ainda emitirá cache e comentários XML que descrevem a implementação, tornando-a valiosa. A principal desvantagem do gerador de origem é que ele emite código adicional em seu assembly, portanto, há o potencial de aumento de tamanho. Quanto mais regexes em seu aplicativo e maiores forem, mais código será emitido para eles. Em algumas situações, assim como RegexOptions.Compiled pode ser desnecessário, também pode ser o gerador de origem. Por exemplo, se você tiver um regex que é necessário apenas raramente e para o qual a taxa de transferência não importa, pode ser mais benéfico apenas contar com o interpretador para esse uso esporádico.

Importante

O .NET 7 inclui um analisador que identifica o uso do Regex que pode ser convertido no gerador de origem e um reparador que faz a conversão para você:

Analisador e fixador RegexGenerator

Confira também