Práticas recomendadas para expressões regulares no .NET

O mecanismo de expressões regulares no .NET é uma ferramenta poderosa e repleta de recursos que processa o texto com base em correspondências de padrões em vez de em comparar e corresponder o texto literal. Na maioria dos casos, ele realiza a correspondência de padrões de forma rápida e eficiente. No entanto, em alguns casos, o mecanismo de expressões regulares pode parecer ser muito lento. Em casos extremos, pode até mesmo parecer parar de responder enquanto processa uma entrada relativamente pequena em um período de horas ou até mesmo dias.

Este artigo descreve algumas das práticas recomendadas que os desenvolvedores podem adotar para garantir que as expressões regulares obtenham o máximo de desempenho.

Aviso

Ao usar System.Text.RegularExpressions para processar entradas não confiáveis, passe um tempo limite. Um usuário mal-intencionado pode fornecer entrada para RegularExpressions, causando um ataque de negação de serviço. APIs ASP.NET Core Framework que usam RegularExpressions passam um tempo limite.

Considere a fonte de entrada

Em geral, as expressões regulares podem aceitar dois tipos de entrada: restrita e não restrita. Uma entrada restrita é o texto proveniente de uma fonte conhecida ou confiável e segue um formato predefinido. Uma entrada irrestrita é um texto proveniente de uma fonte não confiável como um usuário web e que não pode seguir um formato predefinido ou esperado.

Os padrões de expressões regulares geralmente são escritos para corresponder as entradas válidas. Ou seja, os desenvolvedores examinam o texto que desejam corresponder e escrevem um padrão de expressão regular que corresponde a ele. Os desenvolvedores então determinam se esse padrão requer correção ou uma elaboração adicional testando-o com vários itens de entrada válidos. Quando o padrão corresponde a todas as entradas válidas previstas, é declarado como pronto para produção e pode ser incluído em um aplicativo final. Esse método torna um padrão de expressão regular adequado para corresponder uma entrada restrita. No entanto, não é adequado para corresponder à entrada irrestrita.

Para corresponder a uma entrada irrestrita, uma expressão regular deve conseguir manipular três tipos de texto de forma eficiente:

  • Texto que corresponda ao padrão da expressão regular.
  • Texto que não corresponda ao padrão da expressão regular.
  • Texto que quase corresponda ao padrão da expressão regular.

O último tipo de texto é particularmente problemático para uma expressão regular que foi escrita para lidar com entradas restritas. Se essa expressão regular também depender de retrocesso abrangente, o mecanismo de expressões regulares poderá gastar um período fora do normal (em alguns casos, muitas horas ou dias) processando texto aparentemente inócuo.

Aviso

O exemplo a seguir usa uma expressão regular que é sujeita a rastreamento inverso excessivo e que pode rejeitar endereços de email válidos. Você não deve usá-la em uma rotina de validação de email. Se você desejar uma expressão regular que valida endereços de email, confira Como verificar se cadeias de caracteres estão em um formato de email válido.

Por exemplo, pense em uma expressão regular usada, mas problemática, para validar o alias de um endereço de email. A expressão regular ^[0-9A-Z]([-.\w]*[0-9A-Z])*$ é gravada para processar o que é considerado um endereço de email válido. Um endereço de email válido consiste em um caractere alfanumérico seguido por zero ou mais caracteres que podem ser alfanuméricos, pontos ou hifens. A expressão regular deve terminar com um caractere alfanumérico. No entanto, como mostra o exemplo a seguir, embora esta expressão regular identifique entradas válidas com facilidade, seu desempenho é muito ineficiente ao processar uma entrada quase válida:

using System;
using System.Diagnostics;
using System.Text.RegularExpressions;

public class DesignExample
{
    public static void Main()
    {
        Stopwatch sw;
        string[] addresses = { "AAAAAAAAAAA@contoso.com",
                             "AAAAAAAAAAaaaaaaaaaa!@contoso.com" };
        // The following regular expression should not actually be used to
        // validate an email address.
        string pattern = @"^[0-9A-Z]([-.\w]*[0-9A-Z])*$";
        string input;

        foreach (var address in addresses)
        {
            string mailBox = address.Substring(0, address.IndexOf("@"));
            int index = 0;
            for (int ctr = mailBox.Length - 1; ctr >= 0; ctr--)
            {
                index++;

                input = mailBox.Substring(ctr, index);
                sw = Stopwatch.StartNew();
                Match m = Regex.Match(input, pattern, RegexOptions.IgnoreCase);
                sw.Stop();
                if (m.Success)
                    Console.WriteLine("{0,2}. Matched '{1,25}' in {2}",
                                      index, m.Value, sw.Elapsed);
                else
                    Console.WriteLine("{0,2}. Failed  '{1,25}' in {2}",
                                      index, input, sw.Elapsed);
            }
            Console.WriteLine();
        }
    }
}

// The example displays output similar to the following:
//     1. Matched '                        A' in 00:00:00.0007122
//     2. Matched '                       AA' in 00:00:00.0000282
//     3. Matched '                      AAA' in 00:00:00.0000042
//     4. Matched '                     AAAA' in 00:00:00.0000038
//     5. Matched '                    AAAAA' in 00:00:00.0000042
//     6. Matched '                   AAAAAA' in 00:00:00.0000042
//     7. Matched '                  AAAAAAA' in 00:00:00.0000042
//     8. Matched '                 AAAAAAAA' in 00:00:00.0000087
//     9. Matched '                AAAAAAAAA' in 00:00:00.0000045
//    10. Matched '               AAAAAAAAAA' in 00:00:00.0000045
//    11. Matched '              AAAAAAAAAAA' in 00:00:00.0000045
//
//     1. Failed  '                        !' in 00:00:00.0000447
//     2. Failed  '                       a!' in 00:00:00.0000071
//     3. Failed  '                      aa!' in 00:00:00.0000071
//     4. Failed  '                     aaa!' in 00:00:00.0000061
//     5. Failed  '                    aaaa!' in 00:00:00.0000081
//     6. Failed  '                   aaaaa!' in 00:00:00.0000126
//     7. Failed  '                  aaaaaa!' in 00:00:00.0000359
//     8. Failed  '                 aaaaaaa!' in 00:00:00.0000414
//     9. Failed  '                aaaaaaaa!' in 00:00:00.0000758
//    10. Failed  '               aaaaaaaaa!' in 00:00:00.0001462
//    11. Failed  '              aaaaaaaaaa!' in 00:00:00.0002885
//    12. Failed  '             Aaaaaaaaaaa!' in 00:00:00.0005780
//    13. Failed  '            AAaaaaaaaaaa!' in 00:00:00.0011628
//    14. Failed  '           AAAaaaaaaaaaa!' in 00:00:00.0022851
//    15. Failed  '          AAAAaaaaaaaaaa!' in 00:00:00.0045864
//    16. Failed  '         AAAAAaaaaaaaaaa!' in 00:00:00.0093168
//    17. Failed  '        AAAAAAaaaaaaaaaa!' in 00:00:00.0185993
//    18. Failed  '       AAAAAAAaaaaaaaaaa!' in 00:00:00.0366723
//    19. Failed  '      AAAAAAAAaaaaaaaaaa!' in 00:00:00.1370108
//    20. Failed  '     AAAAAAAAAaaaaaaaaaa!' in 00:00:00.1553966
//    21. Failed  '    AAAAAAAAAAaaaaaaaaaa!' in 00:00:00.3223372
Imports System.Diagnostics
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim sw As Stopwatch
        Dim addresses() As String = {"AAAAAAAAAAA@contoso.com",
                                   "AAAAAAAAAAaaaaaaaaaa!@contoso.com"}
        ' The following regular expression should not actually be used to 
        ' validate an email address.
        Dim pattern As String = "^[0-9A-Z]([-.\w]*[0-9A-Z])*$"
        Dim input As String

        For Each address In addresses
            Dim mailBox As String = address.Substring(0, address.IndexOf("@"))
            Dim index As Integer = 0
            For ctr As Integer = mailBox.Length - 1 To 0 Step -1
                index += 1
                input = mailBox.Substring(ctr, index)
                sw = Stopwatch.StartNew()
                Dim m As Match = Regex.Match(input, pattern, RegexOptions.IgnoreCase)
                sw.Stop()
                if m.Success Then
                    Console.WriteLine("{0,2}. Matched '{1,25}' in {2}",
                                      index, m.Value, sw.Elapsed)
                Else
                    Console.WriteLine("{0,2}. Failed  '{1,25}' in {2}",
                                      index, input, sw.Elapsed)
                End If
            Next
            Console.WriteLine()
        Next
    End Sub
End Module
' The example displays output similar to the following:
'     1. Matched '                        A' in 00:00:00.0007122
'     2. Matched '                       AA' in 00:00:00.0000282
'     3. Matched '                      AAA' in 00:00:00.0000042
'     4. Matched '                     AAAA' in 00:00:00.0000038
'     5. Matched '                    AAAAA' in 00:00:00.0000042
'     6. Matched '                   AAAAAA' in 00:00:00.0000042
'     7. Matched '                  AAAAAAA' in 00:00:00.0000042
'     8. Matched '                 AAAAAAAA' in 00:00:00.0000087
'     9. Matched '                AAAAAAAAA' in 00:00:00.0000045
'    10. Matched '               AAAAAAAAAA' in 00:00:00.0000045
'    11. Matched '              AAAAAAAAAAA' in 00:00:00.0000045
'    
'     1. Failed  '                        !' in 00:00:00.0000447
'     2. Failed  '                       a!' in 00:00:00.0000071
'     3. Failed  '                      aa!' in 00:00:00.0000071
'     4. Failed  '                     aaa!' in 00:00:00.0000061
'     5. Failed  '                    aaaa!' in 00:00:00.0000081
'     6. Failed  '                   aaaaa!' in 00:00:00.0000126
'     7. Failed  '                  aaaaaa!' in 00:00:00.0000359
'     8. Failed  '                 aaaaaaa!' in 00:00:00.0000414
'     9. Failed  '                aaaaaaaa!' in 00:00:00.0000758
'    10. Failed  '               aaaaaaaaa!' in 00:00:00.0001462
'    11. Failed  '              aaaaaaaaaa!' in 00:00:00.0002885
'    12. Failed  '             Aaaaaaaaaaa!' in 00:00:00.0005780
'    13. Failed  '            AAaaaaaaaaaa!' in 00:00:00.0011628
'    14. Failed  '           AAAaaaaaaaaaa!' in 00:00:00.0022851
'    15. Failed  '          AAAAaaaaaaaaaa!' in 00:00:00.0045864
'    16. Failed  '         AAAAAaaaaaaaaaa!' in 00:00:00.0093168
'    17. Failed  '        AAAAAAaaaaaaaaaa!' in 00:00:00.0185993
'    18. Failed  '       AAAAAAAaaaaaaaaaa!' in 00:00:00.0366723
'    19. Failed  '      AAAAAAAAaaaaaaaaaa!' in 00:00:00.1370108
'    20. Failed  '     AAAAAAAAAaaaaaaaaaa!' in 00:00:00.1553966
'    21. Failed  '    AAAAAAAAAAaaaaaaaaaa!' in 00:00:00.3223372

Conforme mostrado na saída do exemplo anterior, o mecanismo de expressões regulares processa o alias de email válido quase ao mesmo tempo, independentemente do seu comprimento. Por outro lado, quando o endereço de email quase válido tem mais de cinco caracteres, o tempo de processamento chega a dobrar para cada caractere adicional na cadeia de caracteres. Portanto, uma cadeia de 28 caracteres quase válidos levaria mais de uma hora para ser processada e uma cadeia de 33 caracteres quase válidos demoraria quase um dia.

Como essa expressão regular foi desenvolvida considerando apenas a correspondência com o formato da entrada, ela não leva em consideração entradas que não correspondem ao padrão. Isso, por sua vez, pode permitir que entradas irrestritas quase correspondentes ao padrão da expressão regular prejudiquem significativamente o desempenho.

Para resolver este problema, você pode fazer o seguinte:

  • Ao desenvolver um padrão, você deve considerar como o retrocesso pode afetar o desempenho do mecanismo de expressões regulares, especialmente se a expressão regular for criada para processar entradas sem restrição. Para obter mais informações, consulte a seção Tome conta do retrocesso.

  • Teste rigorosamente sua expressão regular usando entradas inválidas, quase válidas e válidas. Você pode usar Rex para gerar entrada aleatoriamente para uma expressão regular específica. Rex é uma ferramenta de exploração de expressão regular da Microsoft Research.

Trate a instanciação de objetos adequadamente

No coração do modelo de objeto de expressões regulares do .NET está a classe System.Text.RegularExpressions.Regex, a qual representa o mecanismo de expressões regulares. Muitas vezes, o maior fator individual que afeta o desempenho das expressões regulares é a forma como o mecanismo Regex é usado. Definir uma expressão regular envolve o acoplamento vigoroso do mecanismo de expressões regulares com um padrão de expressão regular. Esse processo de acoplamento é caro, seja envolvendo a instanciação de um objeto Regex ao passar para seu constructo um padrão de expressão regular ou chamar um método estático ao passar o padrão de expressão regular e a cadeia de caracteres a ser analisada, é necessariamente caro.

Observação

Para uma análise detalhada das implicações de desempenho do uso de expressões regulares interpretadas e compiladas, confira a postagem no blogOtimizando o desempenho de expressões regulares - Parte II: controle o rastreamento inverso.

É possível acoplar o mecanismo de expressões regulares com um padrão específico de expressão regular e usar o mecanismo para fazer a correspondência com o texto de várias maneiras:

  • Você pode chamar um método estático de correspondência de padrões, Regex.Match(String, String). Este método não requer a instanciação de um objeto de expressão regular.

  • Você pode criar uma instância de um objeto Regex e chamar um método de correspondência de padrões de instância de uma expressão regular interpretada, que é o método padrão para associar o mecanismo de expressão regular a um padrão de expressão regular. Ele ocorre quando um objeto Regex é instanciado sem um argumento options que inclua o sinalizador Compiled.

  • Você pode criar uma instância de um objeto Regex e chamar um método instanciado de correspondência de padrões de uma expressão regular gerada pela origem. Essa técnica é recomendada na maioria dos casos. Para fazer isso, coloque o atributo GeneratedRegexAttribute em um método parcial que retorna Regex.

  • Você pode criar uma instância de um objeto Regex e chamar um método instanciado de correspondência de padrões de uma expressão regular compilada. Os objetos de expressão regular representam padrões compilados quando um objeto Regex é instanciado com um argumento options que inclui o sinalizador Compiled.

A forma específica como você chama métodos de correspondência de expressões regulares pode ter impacto significativo em seu aplicativo. As seções a seguir abordam quando usar chamadas de métodos estáticos, expressões regulares geradas pela origem, expressões regulares interpretadas e expressões regulares compiladas para melhorar o desempenho do seu aplicativo.

Importante

A forma da chamada de método (estático, interpretada, gerada pela origem, compilada) afeta o desempenho se a mesma expressão regular é usada repetidamente em chamadas de método ou se um aplicativo faz uso extensivo de objetos de expressão regular.

Expressões regulares estáticas

Os métodos de expressões regulares estáticas são recomendados como uma alternativa a criar repetidamente instâncias de um objeto de expressão regular com a mesma expressão regular. Diferentemente dos padrões de expressão regular usados pelos objetos de expressão regular, os códigos de operação (opcodes) ou a Common Intermediate Language (CIL) compilada dos padrões usados nas chamadas de método estático são armazenados em cache internamente pelo mecanismo de expressão regular.

Por exemplo, um manipulador de eventos chama frequentemente outro método para validar a entrada do usuário. Este exemplo é mostrado no código a seguir, no qual o evento Button de um controle Click é usado para chamar um método IsValidCurrency, o qual verifica se o usuário inseriu um símbolo de moeda seguido por pelo menos um dígito decimal.

public void OKButton_Click(object sender, EventArgs e)
{
   if (! String.IsNullOrEmpty(sourceCurrency.Text))
      if (RegexLib.IsValidCurrency(sourceCurrency.Text))
         PerformConversion();
      else
         status.Text = "The source currency value is invalid.";
}
Public Sub OKButton_Click(sender As Object, e As EventArgs) _
           Handles OKButton.Click

    If Not String.IsNullOrEmpty(sourceCurrency.Text) Then
        If RegexLib.IsValidCurrency(sourceCurrency.Text) Then
            PerformConversion()
        Else
            status.Text = "The source currency value is invalid."
        End If
    End If
End Sub

Uma implementação ineficiente do método IsValidCurrency é mostrada no exemplo a seguir:

Observação

Observe que cada chamada de método cria uma nova instância de um objeto Regex com o mesmo padrão. Isso, por sua vez, significa que o padrão de expressão regular deve ser recompilado toda vez que o método é chamado.

using System;
using System.Text.RegularExpressions;

public class RegexLib
{
   public static bool IsValidCurrency(string currencyValue)
   {
      string pattern = @"\p{Sc}+\s*\d+";
      Regex currencyRegex = new Regex(pattern);
      return currencyRegex.IsMatch(currencyValue);
   }
}
Imports System.Text.RegularExpressions

Public Module RegexLib
    Public Function IsValidCurrency(currencyValue As String) As Boolean
        Dim pattern As String = "\p{Sc}+\s*\d+"
        Dim currencyRegex As New Regex(pattern)
        Return currencyRegex.IsMatch(currencyValue)
    End Function
End Module

Você deve substituir esse código ineficiente por uma chamada ao método estático Regex.IsMatch(String, String). Esse método elimina a necessidade de criar uma instância de um objeto Regex toda vez que você deseja chamar um método de correspondência de padrões e permite que o mecanismo de expressões regulares recupere uma versão compilada da expressão regular do cache.

using System;
using System.Text.RegularExpressions;

public class RegexLib2
{
   public static bool IsValidCurrency(string currencyValue)
   {
      string pattern = @"\p{Sc}+\s*\d+";
      return Regex.IsMatch(currencyValue, pattern);
   }
}
Imports System.Text.RegularExpressions

Public Module RegexLib
    Public Function IsValidCurrency(currencyValue As String) As Boolean
        Dim pattern As String = "\p{Sc}+\s*\d+"
        Return Regex.IsMatch(currencyValue, pattern)
    End Function
End Module

Por padrão, os 15 padrões de expressões regulares estáticas usados mais recentemente são armazenados no cache. Para aplicativos que requerem um número maior de expressões regulares estáticas armazenadas no cache, o tamanho do cache pode ser ajustado com a definição da propriedade Regex.CacheSize.

A expressão regular \p{Sc}+\s*\d+ que é usada neste exemplo verifica que a cadeia de caracteres de entrada tem um símbolo de moeda e pelo menos um dígito decimal. O padrão é definido conforme mostrado na tabela a seguir:

Padrão Descrição
\p{Sc}+ Corresponde a um ou mais caracteres no símbolo Unicode, categoria de moeda.
\s* Corresponde a zero ou mais caracteres de espaço em branco.
\d+ Corresponde a um ou mais dígitos decimais.

Expressões regulares interpretadas versus geradas pela origem versus compiladas

Os padrões de expressões regulares que não são associados ao mecanismo de expressões regulares com a especificação da opção Compiled são interpretados. Quando um objeto de expressão regular é instanciado, o mecanismo de expressões regulares converte a expressão regular em um conjunto de códigos de operação. Quando um método de instância é chamado, os códigos de operação são convertidos em CIL e executados pelo compilador JIT. Da mesma forma, quando um método estático de expressão regular é chamado e a expressão regular não pode ser encontrada no cache, o mecanismo de expressão regular converte a expressão regular em um conjunto de códigos operacionais e os armazena no cache. Em seguida, ele converte esses códigos de operação em CIL para que o compilador JIT possa executá-los. As expressões regulares interpretadas reduzem o tempo de inicialização ao custo de um tempo de execução mais lento. Por conta desse processo, elas são melhor utilizadas quando a expressão regular é usada em um pequeno número de chamadas de método ou se o número exato de chamadas para métodos de expressão regular é desconhecido, mas com a expectativa de ser pequeno. À medida que número de chamadas de método aumenta, o ganho de desempenho do tempo de inicialização reduzido é superado pela velocidade de execução mais lenta.

Os padrões de expressões regulares associados ao mecanismo de expressões regulares com a especificação da opção Compiled são compilados. Portanto, quando um objeto de expressão regular criar uma instância ou quando um método de expressão regular estática é chamado e a expressão regular não pode ser encontrada no cache, o mecanismo de expressões regulares converte a expressão regular para um conjunto intermediário de códigos de operação. Esses códigos são então convertidos em CIL. Quando um método é chamado, o compilador JIT executa a CIL. Em contraste com as expressões regulares interpretadas, as expressões regulares compiladas aumentam o tempo de inicialização, mas executam os métodos individuais de correspondência padrão de forma mais rápida. Como resultado, o benefício de desempenho que resulta da compilação da expressão regular aumenta em proporção ao número de métodos de expressões regulares chamados.

Os padrões de expressão regular associados ao mecanismo de expressão regular por meio do adorno de um método de retorno Regex com o atributo GeneratedRegexAttribute são gerados pela origem. O gerador de origem, que se conecta ao compilador, emite como código C# uma implementação personalizada derivada de Regexcom lógica semelhante à que RegexOptions.Compiled emite no CIL. 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.

Para resumir, recomendamos que você:

  • Use expressões regulares interpretadas ao chamar os métodos de expressões regulares com uma expressão regular específica com uma relativa frequência.
  • Use expressões regulares geradas pela origem se você estiver usando Regex em C# com argumentos conhecidos em tempo de compilação e estiver usando uma expressão regular específica com relativa frequência.
  • Use expressões regulares compiladas quando você chama métodos de expressão regular com uma expressão regular específica com relativa frequência e está usando o .NET 6 ou uma versão anterior.

É difícil determinar o limite exato no qual as velocidades de execução mais lentas de expressões regulares interpretadas superam os ganhos de seu tempo de inicialização reduzido. Também é difícil determinar o limite em que os tempos de inicialização mais lentos de expressões regulares geradas pela origem ou compiladas superam os ganhos de suas velocidades de execução mais rápidas. Os limites dependem de vários fatores, incluindo a complexidade da expressão regular e dos dados específicos que são processados. Para determinar quais expressões regulares oferecem o melhor desempenho para seu cenário de aplicativo específico, você pode usar a classe Stopwatch para comparar seu tempo de execução.

O exemplo a seguir compara o desempenho de expressões regulares compiladas, geradas pela fonte e interpretadas ao ler as primeiras 10 frases e ao ler todas as frases no texto da Magna Carta e Outros Endereços de William D. Guthrie. Conforme mostrado pela saída do exemplo, quando apenas dez chamadas são feitas para os métodos correspondentes de expressão regular, uma expressão regular interpretada ou gerada pela origem oferece um desempenho melhor do que uma expressão regular compilada. No entanto, uma expressão regular compilada oferece melhor desempenho quando um grande número de chamadas (neste caso, mais 13.000) é feito.

const string Pattern = @"\b(\w+((\r?\n)|,?\s))*\w+[.?:;!]";

static readonly HttpClient s_client = new();

[GeneratedRegex(Pattern, RegexOptions.Singleline)]
private static partial Regex GeneratedRegex();

public async static Task RunIt()
{
    Stopwatch sw;
    Match match;
    int ctr;

    string text =
            await s_client.GetStringAsync("https://www.gutenberg.org/cache/epub/64197/pg64197.txt");

    // Read first ten sentences with interpreted regex.
    Console.WriteLine("10 Sentences with Interpreted Regex:");
    sw = Stopwatch.StartNew();
    Regex int10 = new(Pattern, RegexOptions.Singleline);
    match = int10.Match(text);
    for (ctr = 0; ctr <= 9; ctr++)
    {
        if (match.Success)
            // Do nothing with the match except get the next match.
            match = match.NextMatch();
        else
            break;
    }
    sw.Stop();
    Console.WriteLine("   {0} matches in {1}", ctr, sw.Elapsed);

    // Read first ten sentences with compiled regex.
    Console.WriteLine("10 Sentences with Compiled Regex:");
    sw = Stopwatch.StartNew();
    Regex comp10 = new Regex(Pattern,
                 RegexOptions.Singleline | RegexOptions.Compiled);
    match = comp10.Match(text);
    for (ctr = 0; ctr <= 9; ctr++)
    {
        if (match.Success)
            // Do nothing with the match except get the next match.
            match = match.NextMatch();
        else
            break;
    }
    sw.Stop();
    Console.WriteLine("   {0} matches in {1}", ctr, sw.Elapsed);

    // Read first ten sentences with source-generated regex.
    Console.WriteLine("10 Sentences with Source-generated Regex:");
    sw = Stopwatch.StartNew();

    match = GeneratedRegex().Match(text);
    for (ctr = 0; ctr <= 9; ctr++)
    {
        if (match.Success)
            // Do nothing with the match except get the next match.
            match = match.NextMatch();
        else
            break;
    }
    sw.Stop();
    Console.WriteLine("   {0} matches in {1}", ctr, sw.Elapsed);

    // Read all sentences with interpreted regex.
    Console.WriteLine("All Sentences with Interpreted Regex:");
    sw = Stopwatch.StartNew();
    Regex intAll = new(Pattern, RegexOptions.Singleline);
    match = intAll.Match(text);
    int matches = 0;
    while (match.Success)
    {
        matches++;
        // Do nothing with the match except get the next match.
        match = match.NextMatch();
    }
    sw.Stop();
    Console.WriteLine("   {0:N0} matches in {1}", matches, sw.Elapsed);

    // Read all sentences with compiled regex.
    Console.WriteLine("All Sentences with Compiled Regex:");
    sw = Stopwatch.StartNew();
    Regex compAll = new(Pattern,
                    RegexOptions.Singleline | RegexOptions.Compiled);
    match = compAll.Match(text);
    matches = 0;
    while (match.Success)
    {
        matches++;
        // Do nothing with the match except get the next match.
        match = match.NextMatch();
    }
    sw.Stop();
    Console.WriteLine("   {0:N0} matches in {1}", matches, sw.Elapsed);

    // Read all sentences with source-generated regex.
    Console.WriteLine("All Sentences with Source-generated Regex:");
    sw = Stopwatch.StartNew();
    match = GeneratedRegex().Match(text);
    matches = 0;
    while (match.Success)
    {
        matches++;
        // Do nothing with the match except get the next match.
        match = match.NextMatch();
    }
    sw.Stop();
    Console.WriteLine("   {0:N0} matches in {1}", matches, sw.Elapsed);

    return;
}
/* The example displays output similar to the following:

   10 Sentences with Interpreted Regex:
       10 matches in 00:00:00.0104920
   10 Sentences with Compiled Regex:
       10 matches in 00:00:00.0234604
   10 Sentences with Source-generated Regex:
       10 matches in 00:00:00.0060982
   All Sentences with Interpreted Regex:
       3,427 matches in 00:00:00.1745455
   All Sentences with Compiled Regex:
       3,427 matches in 00:00:00.0575488
   All Sentences with Source-generated Regex:
       3,427 matches in 00:00:00.2698670
*/

O padrão de expressão regular usado neste exemplo, \b(\w+((\r?\n)|,?\s))*\w+[.?:;!], é definido como mostrado na tabela a seguir:

Padrão Descrição
\b Começar a correspondência em um limite de palavra.
\w+ Corresponde a um ou mais caracteres de palavra.
(\r?\n)|,?\s) Corresponde a um zero ou um retorno de carro seguido por um caractere de nova linha, ou zero ou uma vírgula seguida por um caractere de espaço em branco.
(\w+((\r?\n)|,?\s))* Corresponde a zero ou mais ocorrências de um ou mais caracteres de palavra que são seguidos por zero ou por retornos de carro e por um caractere de nova linha ou por zero ou uma vírgula seguida por um caractere de espaço em branco.
\w+ Corresponde a um ou mais caracteres de palavra.
[.?:;!] Corresponde a um ponto, um ponto de interrogação, dois-pontos, ponto e vírgula ou ponto de exclamação.

Tome conta do retrocesso

Normalmente, o mecanismo de expressões regulares usa progressão linear para percorrer uma cadeia de caracteres de entrada e compará-la a uma expressão regular padrão. No entanto, quando quantificadores indefinidos, como *, + e ? são usados em um padrão de expressão regular, o mecanismo de expressões regulares pode ignorar uma parte das correspondências parciais com êxito e retornar ao estado salvo anteriormente para pesquisar uma correspondência com êxito para o padrão inteiro. Esse processo é conhecido como retrocesso.

Dica

Para obter mais informações sobre o retrocesso, consulte Detalhes do comportamento de expressões regulares e Retrocesso. Para obter discussões detalhadas sobre retrocesso, consulte as Melhorias de Expressão Regular no .NET 7 e Otimizando o Desempenho de Expressões Regulares postagens no blog.

O suporte ao retrocesso proporciona poder e flexibilidade às expressões regulares. Ele também coloca a responsabilidade por controlar o funcionamento do mecanismo de expressões regulares nas mãos dos desenvolvedores de expressões regulares. Como os desenvolvedores geralmente não estão cientes dessa responsabilidade, o uso indevido do retrocesso ou a confiança no retrocesso excessivo geralmente exerce o papel mais significativo na degradação do desempenho da expressão regular. Em um cenário de pior caso, o tempo de execução pode dobrar para cada caractere adicional na cadeia de caracteres de entrada. Na verdade, com o uso de rastreamento inverso excessivo, é fácil criar o equivalente programático de um loop infinito se a entrada quase corresponder ao padrão de expressão regular. O mecanismo de expressão regular pode levar horas ou até dias para processar uma cadeia de caracteres de entrada relativamente curta.

Normalmente, os aplicativos pagam uma multa de desempenho por usar o rastreamento inverso, mesmo não sendo essencial para uma correspondência. Por exemplo, a expressão regular \b\p{Lu}\w*\b corresponde a todas as palavras que começam com um caractere maiúsculo, como mostra a tabela a seguir:

Padrão Descrição
\b Começar a correspondência em um limite de palavra.
\p{Lu} Corresponde a um caractere minúsculo.
\w* Corresponde a zero ou mais caracteres de palavra.
\b Termina a correspondência em um limite de palavra.

Como um limite de palavra não é o mesmo que ou um subconjunto de, um caractere de palavra, não há nenhuma possibilidade de o mecanismo de expressões regulares cruzar um limite de palavra ao corresponder caracteres de palavra. Portanto, para essa expressão regular, o rastreamento inverso nunca pode contribuir para o sucesso geral de qualquer correspondência. Ele só pode degradar o desempenho porque o mecanismo de expressão regular é forçado a salvar seu estado para cada correspondência preliminar bem-sucedida de um caractere de palavra.

Se você determinar que o retrocesso não é necessário, você pode desabilitá-lo de algumas maneiras:

  • Definindo a opção RegexOptions.NonBacktracking (introduzida no .NET 7). Para obter mais informações, confira Modo sem retrocesso.

  • Usando o elemento de linguagem (?>subexpression), conhecido como um grupo atômico. O exemplo a seguir analisa uma cadeia de caracteres de entrada usando duas expressões regulares. A primeira, \b\p{Lu}\w*\b, depende do retrocesso. A segunda, \b\p{Lu}(?>\w*)\b, desabilita o retrocesso. Conforme mostrado pela saída do exemplo, ambas produzem o mesmo resultado:

    using System;
    using System.Text.RegularExpressions;
    
    public class BackTrack2Example
    {
        public static void Main()
        {
            string input = "This this word Sentence name Capital";
            string pattern = @"\b\p{Lu}\w*\b";
            foreach (Match match in Regex.Matches(input, pattern))
                Console.WriteLine(match.Value);
    
            Console.WriteLine();
    
            pattern = @"\b\p{Lu}(?>\w*)\b";
            foreach (Match match in Regex.Matches(input, pattern))
                Console.WriteLine(match.Value);
        }
    }
    // The example displays the following output:
    //       This
    //       Sentence
    //       Capital
    //
    //       This
    //       Sentence
    //       Capital
    
    Imports System.Text.RegularExpressions
    
    Module Example
        Public Sub Main()
            Dim input As String = "This this word Sentence name Capital"
            Dim pattern As String = "\b\p{Lu}\w*\b"
            For Each match As Match In Regex.Matches(input, pattern)
                Console.WriteLine(match.Value)
            Next
            Console.WriteLine()
    
            pattern = "\b\p{Lu}(?>\w*)\b"
            For Each match As Match In Regex.Matches(input, pattern)
                Console.WriteLine(match.Value)
            Next
        End Sub
    End Module
    ' The example displays the following output:
    '       This
    '       Sentence
    '       Capital
    '       
    '       This
    '       Sentence
    '       Capital
    

Em muitos casos, o retrocesso é essencial para corresponder um padrão de expressão regular ao texto de entrada. No entanto, o retrocesso excessivo pode prejudicar severamente o desempenho e criar a impressão de que um aplicativo parou de responder. Em particular, esse problema ocorre quando quantificadores são aninhados e o texto que corresponde à subexpressão externa é um subconjunto do texto que corresponde à subexpressão interna.

Aviso

Além de evitar rastreamentos inversos excessivos, você deve usar o recurso de tempo limite para garantir que rastreamentos inversos excessivos não degradem severamente o desempenho da expressão regular. Para obter mais informações, confira a seção Use valores de tempo limite.

Por exemplo, o padrão de expressão regular ^[0-9A-Z]([-.\w]*[0-9A-Z])*\$$ destina-se a corresponder a um número de peça que consiste em pelo menos um caractere alfanumérico. Todos os demais caracteres podem consistir em um caractere alfanumérico, um hífen, um sublinhado ou um ponto, embora o último caractere deva ser alfanumérico. Um cifrão termina o número da peça. Em alguns casos, esse padrão de expressão regular pode exibir um desempenho ruim porque os quantificadores estão aninhados e porque a subexpressão [0-9A-Z] é um subconjunto da subexpressão [-.\w]*.

Nesses casos, você pode otimizar o desempenho da expressão regular ao remover os quantificadores aninhados e substituir a subexpressão externa por uma declaração de lookahead ou lookbehind de largura zero. Asserções lookbehind e lookahead são âncoras. Elas não movem o ponteiro na cadeia de caracteres de entrada, mas fazem uma verificação para checar se uma condição especificada foi atendida. Por exemplo, a expressão regular do número de peça pode ser reescrita como ^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$. Esse padrão de expressão regular é definido conforme mostrado na tabela a seguir:

Padrão Descrição
^ Começar a correspondência no início da cadeia de caracteres de entrada.
[0-9A-Z] Corresponde a um caractere alfanumérico. O número de peça deve consistir em pelo menos este caractere.
[-.\w]* Corresponde a zero ou mais ocorrências de qualquer caractere de palavra, hífen ou ponto.
\$ Corresponde a um cifrão.
(?<=[0-9A-Z]) Avalie atrás do sinal de cifrão final para garantir que o caractere anterior seja alfanumérico.
$ Finalizar a correspondência no final da cadeia de caracteres de entrada.

O exemplo a seguir mostra o uso dessa expressão regular para corresponder a uma matriz que contém possíveis números de blocos:

using System;
using System.Text.RegularExpressions;

public class BackTrack4Example
{
    public static void Main()
    {
        string pattern = @"^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$";
        string[] partNos = { "A1C$", "A4", "A4$", "A1603D$", "A1603D#" };

        foreach (var input in partNos)
        {
            Match match = Regex.Match(input, pattern);
            if (match.Success)
                Console.WriteLine(match.Value);
            else
                Console.WriteLine("Match not found.");
        }
    }
}
// The example displays the following output:
//       A1C$
//       Match not found.
//       A4$
//       A1603D$
//       Match not found.
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim pattern As String = "^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$"
        Dim partNos() As String = {"A1C$", "A4", "A4$", "A1603D$",
                                    "A1603D#"}

        For Each input As String In partNos
            Dim match As Match = Regex.Match(input, pattern)
            If match.Success Then
                Console.WriteLine(match.Value)
            Else
                Console.WriteLine("Match not found.")
            End If
        Next
    End Sub
End Module
' The example displays the following output:
'       A1C$
'       Match not found.
'       A4$
'       A1603D$
'       Match not found.

A linguagem de expressões regulares no .NET inclui os seguintes elementos de linguagem que você pode usar para eliminar quantificadores aninhados. Para obter mais informações, consulte Constructos de agrupamento.

Elemento de linguagem Descrição
(?= subexpression ) Lookahead positivo de largura zero. Avalia à direita da posição atual para determinar se subexpression corresponde à cadeia de caracteres de entrada.
(?! subexpression ) Lookahead negativo de largura zero. Avalia à direita da posição atual para determinar se subexpression não corresponde à cadeia de caracteres de entrada.
(?<= subexpression ) Lookbehind positivo de largura zero. Avalia à esquerda da posição atual para determinar se subexpression corresponde à cadeia de caracteres de entrada.
(?<! subexpression ) Lookbehind negativo de largura zero. Avalia à esquerda da posição atual para determinar se subexpression não corresponde à cadeia de caracteres de entrada.

Use valores de tempo limite

Se suas expressões regulares processarem entradas quase correspondentes ao padrão da expressão regular, elas poderão frequentemente confiar no retrocesso excessivo, o que afeta significativamente o desempenho. Além de considerar cuidadosamente o uso de rastreamento inverso e testar a expressão regular contra entradas quase correspondentes, você deve sempre definir um valor de tempo limite para garantir que o impacto do rastreamento inverso excessivo, caso ocorra, seja minimizado.

O intervalo de tempo limite de expressão regular define o período que o mecanismo de expressão regular procurará por uma única correspondência antes de atingir o tempo limite. Dependendo do padrão de expressão regular e do texto de entrada, o tempo de execução pode exceder o intervalo de tempo limite especificado, mas não passa mais tempo no rastreamento inverso do que o intervalo de tempo limite especificado. O intervalo de tempo limite padrão é Regex.InfiniteMatchTimeout, o que significa que a expressão regular não terá tempo limite. Você pode substituir esse valor e definir um intervalo de tempo limite da seguinte maneira:

Se você tiver definido um intervalo de tempo limite e uma correspondência não for localizada no final do intervalo, o método de expressão regular gerará uma exceção RegexMatchTimeoutException. No manipulador de exceção, você pode optar por tentar fazer novamente a correspondência com um intervalo de tempo limite mais longo, abandonar a tentativa de correspondência e assumir que não há nenhuma correspondência ou abandonar a tentativa de correspondência e registrar as informações de exceção para análise futura.

O exemplo a seguir define um método GetWordData que instancia uma expressão regular com um intervalo de tempo limite de 350 milissegundos para calcular o número de palavras e o número médio de caracteres em uma palavra em um documento de texto. Se a operação de correspondência exceder o tempo limite, o intervalo de tempo limite aumenta em 350 milissegundos e o objeto de Regex criará uma nova instância. Se o novo intervalo de tempo limite exceder 1 segundo, o método gerará novamente a exceção no chamador.

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;

public class TimeoutExample
{
    public static void Main()
    {
        RegexUtilities util = new RegexUtilities();
        string title = "Doyle - The Hound of the Baskervilles.txt";
        try
        {
            var info = util.GetWordData(title);
            Console.WriteLine("Words:               {0:N0}", info.Item1);
            Console.WriteLine("Average Word Length: {0:N2} characters", info.Item2);
        }
        catch (IOException e)
        {
            Console.WriteLine("IOException reading file '{0}'", title);
            Console.WriteLine(e.Message);
        }
        catch (RegexMatchTimeoutException e)
        {
            Console.WriteLine("The operation timed out after {0:N0} milliseconds",
                              e.MatchTimeout.TotalMilliseconds);
        }
    }
}

public class RegexUtilities
{
    public Tuple<int, double> GetWordData(string filename)
    {
        const int MAX_TIMEOUT = 1000;   // Maximum timeout interval in milliseconds.
        const int INCREMENT = 350;      // Milliseconds increment of timeout.

        List<string> exclusions = new List<string>(new string[] { "a", "an", "the" });
        int[] wordLengths = new int[29];        // Allocate an array of more than ample size.
        string input = null;
        StreamReader sr = null;
        try
        {
            sr = new StreamReader(filename);
            input = sr.ReadToEnd();
        }
        catch (FileNotFoundException e)
        {
            string msg = String.Format("Unable to find the file '{0}'", filename);
            throw new IOException(msg, e);
        }
        catch (IOException e)
        {
            throw new IOException(e.Message, e);
        }
        finally
        {
            if (sr != null) sr.Close();
        }

        int timeoutInterval = INCREMENT;
        bool init = false;
        Regex rgx = null;
        Match m = null;
        int indexPos = 0;
        do
        {
            try
            {
                if (!init)
                {
                    rgx = new Regex(@"\b\w+\b", RegexOptions.None,
                                    TimeSpan.FromMilliseconds(timeoutInterval));
                    m = rgx.Match(input, indexPos);
                    init = true;
                }
                else
                {
                    m = m.NextMatch();
                }
                if (m.Success)
                {
                    if (!exclusions.Contains(m.Value.ToLower()))
                        wordLengths[m.Value.Length]++;

                    indexPos += m.Length + 1;
                }
            }
            catch (RegexMatchTimeoutException e)
            {
                if (e.MatchTimeout.TotalMilliseconds < MAX_TIMEOUT)
                {
                    timeoutInterval += INCREMENT;
                    init = false;
                }
                else
                {
                    // Rethrow the exception.
                    throw;
                }
            }
        } while (m.Success);

        // If regex completed successfully, calculate number of words and average length.
        int nWords = 0;
        long totalLength = 0;

        for (int ctr = wordLengths.GetLowerBound(0); ctr <= wordLengths.GetUpperBound(0); ctr++)
        {
            nWords += wordLengths[ctr];
            totalLength += ctr * wordLengths[ctr];
        }
        return new Tuple<int, double>(nWords, totalLength / nWords);
    }
}
Imports System.Collections.Generic
Imports System.IO
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim util As New RegexUtilities()
        Dim title As String = "Doyle - The Hound of the Baskervilles.txt"
        Try
            Dim info = util.GetWordData(title)
            Console.WriteLine("Words:               {0:N0}", info.Item1)
            Console.WriteLine("Average Word Length: {0:N2} characters", info.Item2)
        Catch e As IOException
            Console.WriteLine("IOException reading file '{0}'", title)
            Console.WriteLine(e.Message)
        Catch e As RegexMatchTimeoutException
            Console.WriteLine("The operation timed out after {0:N0} milliseconds",
                              e.MatchTimeout.TotalMilliseconds)
        End Try
    End Sub
End Module

Public Class RegexUtilities
    Public Function GetWordData(filename As String) As Tuple(Of Integer, Double)
        Const MAX_TIMEOUT As Integer = 1000  ' Maximum timeout interval in milliseconds.
        Const INCREMENT As Integer = 350     ' Milliseconds increment of timeout.

        Dim exclusions As New List(Of String)({"a", "an", "the"})
        Dim wordLengths(30) As Integer        ' Allocate an array of more than ample size.
        Dim input As String = Nothing
        Dim sr As StreamReader = Nothing
        Try
            sr = New StreamReader(filename)
            input = sr.ReadToEnd()
        Catch e As FileNotFoundException
            Dim msg As String = String.Format("Unable to find the file '{0}'", filename)
            Throw New IOException(msg, e)
        Catch e As IOException
            Throw New IOException(e.Message, e)
        Finally
            If sr IsNot Nothing Then sr.Close()
        End Try

        Dim timeoutInterval As Integer = INCREMENT
        Dim init As Boolean = False
        Dim rgx As Regex = Nothing
        Dim m As Match = Nothing
        Dim indexPos As Integer = 0
        Do
            Try
                If Not init Then
                    rgx = New Regex("\b\w+\b", RegexOptions.None,
                                    TimeSpan.FromMilliseconds(timeoutInterval))
                    m = rgx.Match(input, indexPos)
                    init = True
                Else
                    m = m.NextMatch()
                End If
                If m.Success Then
                    If Not exclusions.Contains(m.Value.ToLower()) Then
                        wordLengths(m.Value.Length) += 1
                    End If
                    indexPos += m.Length + 1
                End If
            Catch e As RegexMatchTimeoutException
                If e.MatchTimeout.TotalMilliseconds < MAX_TIMEOUT Then
                    timeoutInterval += INCREMENT
                    init = False
                Else
                    ' Rethrow the exception.
                    Throw
                End If
            End Try
        Loop While m.Success

        ' If regex completed successfully, calculate number of words and average length.
        Dim nWords As Integer
        Dim totalLength As Long

        For ctr As Integer = wordLengths.GetLowerBound(0) To wordLengths.GetUpperBound(0)
            nWords += wordLengths(ctr)
            totalLength += ctr * wordLengths(ctr)
        Next
        Return New Tuple(Of Integer, Double)(nWords, totalLength / nWords)
    End Function
End Class

Capture somente quando necessário

As expressões regulares no .NET oferecem suporte a constructos de agrupamento, que permitem a você agrupar um padrão de expressão regular em uma ou mais subexpressões. Os constructos de agrupamento mais usados na linguagem de expressões regulares no .NET são (subexpression), que define um grupo de captura numerado, e (?<name>subexpression), que define um grupo de captura nomeado. Os construtores de agrupamento são essenciais para criar referências reversas e definir uma subexpressão à qual um quantificador é aplicado.

No entanto, o uso desses elementos de linguagem tem um custo. Eles fazem com que o objeto GroupCollection retornado pela propriedade Match.Groups seja preenchido com as capturas nomeadas ou sem nome mais recentes. Se um único constructo de agrupamento capturar várias substrings de caracteres na cadeia de caracteres de entrada, também preenchem o objeto CaptureCollection retornado pela propriedade Group.Captures de um grupo de captura específico com vários objetos Capture.

Muitas vezes, os constructos de agrupamento são usados em uma expressão regular apenas para que os quantificadores possam ser aplicados a eles. Os grupos capturados por essas subexpressões não são usados posteriormente. Por exemplo, a expressão regular \b(\w+[;,]?\s?)+[.?!] é criada para capturar uma frase inteira. A tabela a seguir descreve os elementos de linguagem nesse padrão de expressão regular e seu efeito nas coleções Match e Match.Groups do objeto Group.Captures:

Padrão Descrição
\b Começar a correspondência em um limite de palavra.
\w+ Corresponde a um ou mais caracteres de palavra.
[;,]? Corresponde a zero ou uma vírgula ou ponto e vírgula.
\s? Corresponde a zero ou a um caractere de espaço em branco.
(\w+[;,]?\s?)+ Corresponde a uma ou mais ocorrências de um ou mais caracteres de palavra seguidos por uma vírgula opcional ou por ponto e vírgula seguido por um caractere de espaço em branco opcional. Este padrão define o primeiro grupo de captura, que é necessário para que a combinação de vários caracteres de palavra (ou seja, uma palavra) seguido por um símbolo de pontuação opcional seja repetida até que o mecanismo de expressões regulares atinja o final de uma sentença.
[.?!] Corresponde a um ponto, um ponto de interrogação ou um ponto de exclamação.

Como o exemplo a seguir mostra, quando uma correspondência é encontrada, os objetos GroupCollection e de CaptureCollection são preenchidos com as capturas da correspondência. Nesse caso, o grupo de captura (\w+[;,]?\s?) existe para que o quantificador + possa ser aplicado a ele, o que permite que o padrão de expressão regular corresponda a cada palavra em uma sentença. Caso contrário, ele corresponderia à última palavra em uma sentença.

using System;
using System.Text.RegularExpressions;

public class Group1Example
{
    public static void Main()
    {
        string input = "This is one sentence. This is another.";
        string pattern = @"\b(\w+[;,]?\s?)+[.?!]";

        foreach (Match match in Regex.Matches(input, pattern))
        {
            Console.WriteLine("Match: '{0}' at index {1}.",
                              match.Value, match.Index);
            int grpCtr = 0;
            foreach (Group grp in match.Groups)
            {
                Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                                  grpCtr, grp.Value, grp.Index);
                int capCtr = 0;
                foreach (Capture cap in grp.Captures)
                {
                    Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                      capCtr, cap.Value, cap.Index);
                    capCtr++;
                }
                grpCtr++;
            }
            Console.WriteLine();
        }
    }
}
// The example displays the following output:
//       Match: 'This is one sentence.' at index 0.
//          Group 0: 'This is one sentence.' at index 0.
//             Capture 0: 'This is one sentence.' at 0.
//          Group 1: 'sentence' at index 12.
//             Capture 0: 'This ' at 0.
//             Capture 1: 'is ' at 5.
//             Capture 2: 'one ' at 8.
//             Capture 3: 'sentence' at 12.
//
//       Match: 'This is another.' at index 22.
//          Group 0: 'This is another.' at index 22.
//             Capture 0: 'This is another.' at 22.
//          Group 1: 'another' at index 30.
//             Capture 0: 'This ' at 22.
//             Capture 1: 'is ' at 27.
//             Capture 2: 'another' at 30.
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim input As String = "This is one sentence. This is another."
        Dim pattern As String = "\b(\w+[;,]?\s?)+[.?!]"

        For Each match As Match In Regex.Matches(input, pattern)
            Console.WriteLine("Match: '{0}' at index {1}.",
                              match.Value, match.Index)
            Dim grpCtr As Integer = 0
            For Each grp As Group In match.Groups
                Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                                  grpCtr, grp.Value, grp.Index)
                Dim capCtr As Integer = 0
                For Each cap As Capture In grp.Captures
                    Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                      capCtr, cap.Value, cap.Index)
                    capCtr += 1
                Next
                grpCtr += 1
            Next
            Console.WriteLine()
        Next
    End Sub
End Module
' The example displays the following output:
'       Match: 'This is one sentence.' at index 0.
'          Group 0: 'This is one sentence.' at index 0.
'             Capture 0: 'This is one sentence.' at 0.
'          Group 1: 'sentence' at index 12.
'             Capture 0: 'This ' at 0.
'             Capture 1: 'is ' at 5.
'             Capture 2: 'one ' at 8.
'             Capture 3: 'sentence' at 12.
'       
'       Match: 'This is another.' at index 22.
'          Group 0: 'This is another.' at index 22.
'             Capture 0: 'This is another.' at 22.
'          Group 1: 'another' at index 30.
'             Capture 0: 'This ' at 22.
'             Capture 1: 'is ' at 27.
'             Capture 2: 'another' at 30.

Quando você usa subexpressões apenas para aplicar quantificadores a elas e não está interessado em texto capturado, desabilite as capturas de grupo. Por exemplo, o elemento de linguagem (?:subexpression) evita que o grupo ao qual ele se aplica capture subcadeias de caracteres correspondidas. No exemplo a seguir, o padrão de expressão regular do exemplo anterior é alterado para \b(?:\w+[;,]?\s?)+[.?!]. Conforme mostrado pela saída, ele impede que o mecanismo de expressões regulares preencha as coleções GroupCollection e CaptureCollection:

using System;
using System.Text.RegularExpressions;

public class Group2Example
{
    public static void Main()
    {
        string input = "This is one sentence. This is another.";
        string pattern = @"\b(?:\w+[;,]?\s?)+[.?!]";

        foreach (Match match in Regex.Matches(input, pattern))
        {
            Console.WriteLine("Match: '{0}' at index {1}.",
                              match.Value, match.Index);
            int grpCtr = 0;
            foreach (Group grp in match.Groups)
            {
                Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                                  grpCtr, grp.Value, grp.Index);
                int capCtr = 0;
                foreach (Capture cap in grp.Captures)
                {
                    Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                      capCtr, cap.Value, cap.Index);
                    capCtr++;
                }
                grpCtr++;
            }
            Console.WriteLine();
        }
    }
}
// The example displays the following output:
//       Match: 'This is one sentence.' at index 0.
//          Group 0: 'This is one sentence.' at index 0.
//             Capture 0: 'This is one sentence.' at 0.
//
//       Match: 'This is another.' at index 22.
//          Group 0: 'This is another.' at index 22.
//             Capture 0: 'This is another.' at 22.
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim input As String = "This is one sentence. This is another."
        Dim pattern As String = "\b(?:\w+[;,]?\s?)+[.?!]"

        For Each match As Match In Regex.Matches(input, pattern)
            Console.WriteLine("Match: '{0}' at index {1}.",
                              match.Value, match.Index)
            Dim grpCtr As Integer = 0
            For Each grp As Group In match.Groups
                Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                                  grpCtr, grp.Value, grp.Index)
                Dim capCtr As Integer = 0
                For Each cap As Capture In grp.Captures
                    Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                      capCtr, cap.Value, cap.Index)
                    capCtr += 1
                Next
                grpCtr += 1
            Next
            Console.WriteLine()
        Next
    End Sub
End Module
' The example displays the following output:
'       Match: 'This is one sentence.' at index 0.
'          Group 0: 'This is one sentence.' at index 0.
'             Capture 0: 'This is one sentence.' at 0.
'       
'       Match: 'This is another.' at index 22.
'          Group 0: 'This is another.' at index 22.
'             Capture 0: 'This is another.' at 22.

É possível desabilitar as capturas de uma das seguintes formas:

  • Use o elemento de linguagem (?:subexpression). Esse elemento impede a captura de subcadeias de caracteres correspondidas no grupo ao qual se ele aplica. Ele não desabilita capturas de substring de caracteres em grupos aninhados.

  • Use a opção ExplicitCapture. Ela desabilita todas as capturas sem nome ou implícitas no padrão de expressão regular. Quando você usa essa opção, somente as subcadeias de caracteres que correspondem aos grupos nomeados definidos com o elemento de linguagem (?<name>subexpression) podem ser capturadas. O sinalizador ExplicitCapture pode ser passado para o parâmetro options de um construtor de classe Regex ou para o parâmetro options de um método de correspondência estática Regex.

  • Use a opção n no elemento de linguagem (?imnsx). Esta opção desabilita todas as capturas sem nome ou implícitas a partir do ponto no padrão de expressão regular em que o elemento aparece. As capturas são desabilitadas até o final do padrão ou até a opção (-n) permitir capturas sem nome ou implícitas. Para saber mais, confira Constructos diversos.

  • Use a opção n no elemento de linguagem (?imnsx:subexpression). Esta opção desativa todas as capturas sem nome ou implícitas em subexpression. As capturas por grupos de capturas aninhadas sem nome ou implícitas também são desabilitadas.

Acesso thread-safe

A própria classe Regex é thread-safe e imutável (somente leitura). Ou seja, os objetos de Regex podem ser criados em qualquer thread e compartilhados entre os threads. Métodos correspondentes podem ser chamados de qualquer thread e nunca alteram nenhum estado global.

No entanto, os objetos de resultado (Match e MatchCollection) retornados por Regex devem ser usados em um único thread. Embora muitos desses objetos sejam logicamente imutáveis, suas implementações poderiam atrasar a computação de alguns resultados para melhorar o desempenho e, como resultado, os chamadores devem serializar o acesso a eles.

Se você precisar compartilhar os objetos de resultado Regex em vários threads, esses objetos poderão ser convertidos em instâncias thread-safe chamando seus métodos sincronizados. Com exceção dos enumeradores, todas as classes de expressões regulares são thread-safe ou podem ser convertidas em objetos thread-safe por um método sincronizado.

Os enumeradores são a única exceção. Você precisa serializar as chamadas a enumeradores de coleções. A regra é que se uma coleção pode ser enumerada em mais de um thread simultaneamente, você deve sincronizar os métodos do enumerador no objeto raiz da coleção percorrida pelo enumerador.

Título Descrição
Detalhes do comportamento de expressões regulares Examina a implementação do mecanismo de expressões regulares no .NET. O artigo trata da flexibilidade de expressões regulares e explica a responsabilidade do desenvolvedor para garantir o funcionamento eficiente e robusto do mecanismo de expressões regulares.
Retrocesso Explica o que é o retrocesso é como ele afeta o desempenho da expressão regular e examina os elementos de linguagem que fornecem alternativas ao retrocesso.
Linguagem de expressões regulares – referência rápida Descreve os elementos de linguagem de expressões regulares do .NET e fornece links para a documentação detalhada de cada elemento da linguagem.