Compartir a través de


Procedimientos recomendados para expresiones regulares en .NET

El motor de expresiones regulares de .NET es una herramienta eficaz y completa que procesa texto basado en coincidencias de patrones en lugar de comparar y buscar texto literal coincidente. En la mayoría de los casos, realiza la coincidencia de patrones de forma rápida y eficaz. Sin embargo, en algunos casos, el motor de expresiones regulares puede parecer lento. En casos extremos, incluso puede parecer dejar de responder a medida que procesa una entrada relativamente pequeña durante el transcurso de horas o incluso días.

En este artículo se describen algunos de los procedimientos recomendados que los desarrolladores pueden adoptar para garantizar que sus expresiones regulares logren un rendimiento óptimo.

Advertencia

Cuando se usa System.Text.RegularExpressions para procesar entradas que no son de confianza, pase un tiempo de expiración. Un usuario malintencionado puede proporcionar una entrada a RegularExpressions, lo que provoca un ataque por denegación de servicio. Las API del marco ASP.NET Core en las que se usa RegularExpressions pasan un tiempo de expiración.

Considere el origen de entrada.

En general, las expresiones regulares pueden aceptar dos tipos de entrada: restringidos o sin restricciones. La entrada restringida es un texto que se origina en un origen conocido o confiable y sigue un formato predefinido. La entrada sin restricciones es un texto que se origina en un origen no confiable, como un usuario web, y es posible que no siga un formato predefinido o esperado.

Los patrones de expresiones regulares a menudo se escriben para que coincidan con la entrada válida. Es decir, los desarrolladores examinan el texto que quieren que coincidan y, a continuación, escriben un patrón de expresión regular que coincida con él. A continuación, los desarrolladores determinan si este patrón requiere corrección o posterior elaboración mediante la prueba con varios elementos de entrada válidos. Cuando el patrón coincide con todas las entradas válidas supuestas, se declara que está lista para producción y se puede incluir en una aplicación publicada. Este enfoque hace que un patrón de expresión regular sea adecuado para la coincidencia de entradas restringidas. Sin embargo, no es adecuado para datos de entrada sin restricciones coincidentes.

Para hacer coincidir la entrada sin restricciones, una expresión regular debe controlar tres tipos de texto de forma eficaz:

  • Texto que coincide con el patrón de expresión regular.
  • Texto que no coincide con el patrón de expresión regular.
  • Texto que casi coincide con el patrón de expresión regular.

El último tipo de texto es especialmente problemático para una expresión regular escrita para controlar la entrada restringida. Si esa expresión regular también se basa en un retroceso extenso, el motor de expresiones regulares puede pasar una cantidad de tiempo inordinar (en algunos casos, muchas horas o días) procesando texto aparentemente inocuo.

Advertencia

En el ejemplo siguiente se usa una expresión regular que es propensa a un retroceso excesivo y que es probable que rechace direcciones de correo electrónico válidas. No debe usarlo en una rutina de validación de correo electrónico. Si desea una expresión regular que valide las direcciones de correo electrónico, vea Cómo: Comprobar que las cadenas están en formato de correo electrónico válido.

Por ejemplo, considere una expresión regular común pero problemática para validar el alias de una dirección de correo electrónico. La expresión ^[0-9A-Z]([-.\w]*[0-9A-Z])*$ regular se escribe para procesar lo que se considera una dirección de correo electrónico válida. Una dirección de correo electrónico válida consta de un carácter alfanumérico, seguido de cero o más caracteres que pueden ser alfanuméricos, puntos o guiones. La expresión regular debe terminar con un carácter alfanumérico. Sin embargo, como se muestra en el ejemplo siguiente, aunque esta expresión regular controla fácilmente la entrada válida, su rendimiento es ineficaz cuando procesa una entrada casi 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

Como se muestra en la salida del ejemplo anterior, el motor de expresiones regulares procesa el alias de correo electrónico válido en aproximadamente el mismo intervalo de tiempo, independientemente de su longitud. Por otro lado, cuando la dirección de correo electrónico casi válida tiene más de cinco caracteres, el tiempo de procesamiento se duplica aproximadamente para cada carácter adicional de la cadena. Por lo tanto, una cadena de 28 caracteres casi válida tardaría más de una hora en procesarse y una cadena de 33 caracteres casi válida tardaría casi un día en procesarse.

Dado que esta expresión regular se desarrolló únicamente teniendo en cuenta el formato de entrada que se va a hacer coincidir, no puede tener en cuenta la entrada que no coincide con el patrón. Este descuido, a su vez, puede permitir una entrada sin restricciones que se asemeja casi al patrón de expresión regular, deteriorando significativamente el rendimiento.

Para solucionar este problema, puede hacer lo siguiente:

  • Al desarrollar un patrón, debe tener en cuenta cómo el retroceso podría afectar al rendimiento del motor de expresiones regulares, especialmente si la expresión regular está diseñada para procesar entradas sin restricciones. Para obtener más información, consulte la sección Controlar el retroceso.

  • Pruebe exhaustivamente la expresión regular usando datos de entrada no válidos, casi válidos y válidos. Puede usar Rex para generar de forma aleatoria la entrada para una expresión regular determinada. Rex es una herramienta de exploración de expresiones regulares de Microsoft Research.

Controlar la creación de instancias de objeto correctamente

En el corazón del modelo de objeto de expresiones regulares de .NET está la clase System.Text.RegularExpressions.Regex, que representa el motor de expresiones regulares. A menudo, el factor más grande que afecta al rendimiento de expresiones regulares es la forma en que se usa el Regex motor. Definir una expresión regular implica acoplar estrechamente el motor de expresiones regulares con un patrón de expresión regular. Ese proceso de acoplamiento es costoso, ya sea que implique crear una instancia de un objeto Regex pasando a su constructor un patrón de expresión regular, o llamar a un método estático pasando el patrón de expresión regular y la cadena que se va a analizar.

Nota:

Para obtener una explicación detallada de las implicaciones de rendimiento del uso de expresiones regulares interpretadas y compiladas, consulte la entrada de blog Optimización del rendimiento de expresiones regulares, parte II: Tomar el control del retroceso.

Puede acoplar el motor de expresiones regulares con un patrón de expresión regular determinado y, a continuación, usar el motor para que coincida con el texto de varias maneras:

  • Puede llamar a un método estático de coincidencia de patrones, como Regex.Match(String, String). Este método no requiere la creación de instancias de un objeto de expresión regular.

  • Puede instanciar un objeto Regex y llamar a un método de coincidencia de patrones de una expresión regular interpretada, que es el método predeterminado para enlazar el motor de expresiones regulares a un patrón de expresión regular. Se produce cuando se crea una instancia de un objeto Regex sin un options argumento que incluye el indicador Compiled.

  • Puede crear instancias de un objeto Regex y llamar a una instancia de un método de coincidencia de patrones de una expresión regular generada por código fuente. Esta técnica se recomienda en la mayoría de los casos. Para ello, coloque el GeneratedRegexAttribute atributo en un método parcial que devuelva Regex.

  • Puede crear una instancia de un objeto Regex y llamar a un método de coincidencia de patrones para instancias de una expresión regular compilada. Los objetos de expresiones regulares representan patrones compilados cuando se instancia un Regex objeto con un options argumento que incluye la Compiled bandera.

La forma concreta en la que se llama a métodos de coincidencia de expresiones regulares puede afectar al rendimiento de la aplicación. En las secciones siguientes se describe cuándo usar llamadas a métodos estáticos, expresiones regulares generadas por el origen, expresiones regulares interpretadas y expresiones regulares compiladas para mejorar el rendimiento de la aplicación.

Importante

La forma de la llamada al método (estática, interpretada, generada por el origen y compilada) afecta al rendimiento si la misma expresión regular se usa repetidamente en llamadas a métodos o si una aplicación hace un uso extenso de objetos de expresión regular.

Expresiones regulares estáticas

Los métodos de expresiones regulares estáticas se recomiendan como alternativa a crear instancias repetidas de un objeto de expresión regular con la misma expresión regular. A diferencia de los patrones de expresiones regulares usados por los objetos de expresión regular, los códigos de operación (códigos de operación) o el lenguaje intermedio común compilado (CIL) de los patrones usados en las llamadas a métodos estáticos se almacenan en caché internamente por el motor de expresiones regulares.

Por ejemplo, un controlador de eventos llama con frecuencia a otro método para validar la entrada del usuario. Este ejemplo se refleja en el código siguiente, en el que se usa el evento de Button un Click control para llamar a un método denominado IsValidCurrency, que comprueba si el usuario ha escrito un símbolo de moneda seguido de al menos un 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

En el ejemplo siguiente se muestra una implementación ineficaz del IsValidCurrency método:

Nota:

Cada llamada al método reinstancia un objeto Regex con el mismo patrón. Esto, a su vez, significa que el patrón de expresión regular se debe volver a compilar cada vez que se llama al método .

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

Debe reemplazar el código ineficaz anterior por una llamada al método estático Regex.IsMatch(String, String) . Este enfoque elimina la necesidad de crear instancias de un Regex objeto cada vez que quiera llamar a un método de coincidencia de patrones y permite al motor de expresiones regulares recuperar una versión compilada de la expresión regular de su caché.

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

De forma predeterminada, los últimos 15 patrones de expresiones regulares estáticas usados se almacenan en caché. En el caso de las aplicaciones que requieren un mayor número de expresiones regulares estáticas almacenadas en caché, el tamaño de la memoria caché se puede ajustar estableciendo la Regex.CacheSize propiedad .

La expresión \p{Sc}+\s*\d+ regular que se usa en este ejemplo comprueba que la cadena de entrada tiene un símbolo de moneda y al menos un dígito decimal. El patrón se define como se muestra en la tabla siguiente:

Modelo Descripción
\p{Sc}+ Coincide con uno o varios caracteres en la categoría Símbolo Unicode, Moneda.
\s* Busca coincidencias con cero o más caracteres de espacio en blanco.
\d+ Buscar coincidencias con uno o más dígitos decimales.

Expresiones regulares interpretadas frente a generadas por código fuente frente a expresiones regulares compiladas

Los patrones de expresión regular que no están enlazados al motor de expresiones regulares a través de la especificación de la Compiled opción se interpretan. Cuando se crea una instancia de un objeto de expresión regular, el motor de expresiones regulares convierte la expresión regular en un conjunto de códigos de operación. Cuando se llama a un método de instancia, los códigos de operación se convierten en CIL y se ejecutan mediante el compilador JIT. Del mismo modo, cuando se llama a un método de expresión regular estática y no se encuentra la expresión regular en la memoria caché, el motor de expresiones regulares convierte la expresión regular en un conjunto de códigos de operación y los almacena en la memoria caché. A continuación, convierte estos códigos de operación en CIL para que el compilador JIT pueda ejecutarlos. Las expresiones regulares interpretadas reducen el tiempo de inicio a costa del tiempo de ejecución más lento. Debido a este proceso, se usan mejor cuando se usa la expresión regular en un pequeño número de llamadas a métodos, o si se desconoce el número exacto de llamadas a métodos de expresión regular, pero se espera que sea pequeño. A medida que aumenta el número de llamadas de método, la ganancia de rendimiento a partir del tiempo de inicio reducido se agota por la velocidad de ejecución más lenta.

Los patrones de expresión regular que están enlazados al motor de expresiones regulares mediante la especificación de la Compiled opción se compilan. Por lo tanto, cuando se crea una instancia de un objeto de expresión regular o cuando se llama a un método de expresión regular estática y no se encuentra la expresión regular en la memoria caché, el motor de expresiones regulares convierte la expresión regular en un conjunto intermedio de códigos de operación. Estos códigos se convierten entonces a CIL. Cuando se llama a un método, el compilador JIT ejecuta la CIL. A diferencia de las expresiones regulares interpretadas, las expresiones regulares compiladas aumentan el tiempo de inicio, pero ejecutan métodos individuales de coincidencia de patrones más rápidos. Como resultado, la ventaja de rendimiento que resulta de la compilación de la expresión regular aumenta en proporción al número de métodos de expresión regular llamados.

Los patrones de expresiones regulares que están enlazados al motor de expresiones regulares mediante el adorno de un método que devuelve Regex con el atributo GeneratedRegexAttribute se generan por código fuente. El generador de origen, que se conecta al compilador, emite como código de C# una implementación derivada personalizada Regexcon lógica similar a la que RegexOptions.Compiled emite en la CIL. Se obtienen todas las ventajas de rendimiento de RegexOptions.Compiled (más, de hecho) y las ventajas de inicio de Regex.CompileToAssembly, pero sin la complejidad de CompileToAssembly. El código fuente que se emite forma parte del proyecto, lo que significa que también es fácil de ver y depurar.

Para resumir, le recomendamos que:

  • Use expresiones regulares interpretadas al llamar a métodos de expresión regular con una expresión regular específica con poca frecuencia.
  • Use expresiones regulares generadas a partir de la fuente si usa Regex en C# con argumentos conocidos en tiempo de compilación y utiliza una expresión regular específica con relativa frecuencia.
  • Use expresiones regulares compiladas cuando llame a métodos de expresión regular con una expresión regular específica con relativamente frecuencia y use .NET 6 o una versión anterior.

Es difícil determinar el umbral exacto en el que las velocidades de ejecución más lentas de las expresiones regulares interpretadas superan las ganancias de su tiempo de inicio reducido. También es difícil determinar el umbral en el que los tiempos de inicio más lentos de las expresiones regulares generadas por el origen o compiladas superan las ganancias de sus velocidades de ejecución más rápidas. Los umbrales dependen de varios factores, incluida la complejidad de la expresión regular y los datos específicos que procesa. Para determinar qué expresiones regulares ofrecen el mejor rendimiento para su escenario de aplicación concreto, puede usar la Stopwatch clase para comparar sus tiempos de ejecución.

En el ejemplo siguiente se compara el rendimiento de expresiones regulares compiladas, generadas a partir de código fuente e interpretadas al leer las primeras 10 oraciones y al leer todas las oraciones del texto de la obra de William D. Guthrie, Magna Carta, y otras conferencias. Como se muestra en la salida del ejemplo, cuando solo se realizan 10 llamadas a métodos de coincidencia de expresiones regulares, una expresión regular interpretada o generada por el origen ofrece un mejor rendimiento que una expresión regular compilada. Sin embargo, una expresión regular compilada ofrece un mejor rendimiento cuando se realiza un gran número de llamadas (en este caso, más de 13 000).

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($"   {ctr} matches in {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($"   {ctr} matches in {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($"   {ctr} matches in {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($"   {matches:N0} matches in {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($"   {matches:N0} matches in {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($"   {matches:N0} matches in {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
*/

El patrón de expresión regular usado en el ejemplo, \b(\w+((\r?\n)|,?\s))*\w+[.?:;!], se define como se muestra en la tabla siguiente:

Modelo Descripción
\b Iniciar la búsqueda de coincidencias en un límite de palabras.
\w+ Busca coincidencias con uno o más caracteres alfabéticos.
(\r?\n)|,?\s) Busca coincidencias con cero o un retorno de carro seguido de un carácter de nueva línea, o cero o una coma seguida de un carácter de espacio en blanco.
(\w+((\r?\n)|,?\s))* Busca coincidencias con cero o más apariciones de uno o más caracteres alfabéticos que van seguidos de cero o un retorno de carro y un carácter de nueva línea, o de cero o una coma seguida de un carácter de espacio en blanco.
\w+ Busca coincidencias con uno o más caracteres alfabéticos.
[.?:;!] Busca coincidencias con un punto, un signo de interrogación, dos puntos, punto y coma o un signo de exclamación.

Controlar el retroceso

Normalmente, el motor de expresiones regulares usa la progresión lineal para desplazarse por una cadena de entrada y compararla con un patrón de expresión regular. Sin embargo, cuando se usan cuantificadores indeterminados como *, +y ? en un patrón de expresión regular, el motor de expresiones regulares podría renunciar a una parte de coincidencias parciales correctas y volver a un estado guardado previamente para buscar una coincidencia correcta para todo el patrón. Este proceso se conoce como retroceso.

Sugerencia

Para obtener más información sobre el retroceso, vea Detalles del comportamiento de expresiones regulares y Retroceso. Para obtener discusiones detalladas sobre el retroceso, consulte las entradas del blog Mejoras de expresiones regulares en .NET 7 y Optimización del rendimiento de expresiones regulares .

La compatibilidad con el retroceso aporta a las expresiones regulares eficacia y flexibilidad. También pone la responsabilidad de controlar el funcionamiento del motor de expresiones regulares en manos de los desarrolladores de expresiones regulares. Puesto que los desarrolladores no suelen ser conscientes de esta responsabilidad, su uso incorrecto del retroceso o su dependencia de un retroceso excesivo suele desempeñar el rol más significativo en la degradación del rendimiento de las expresiones regulares. En el peor de los casos, el tiempo de ejecución puede duplicarse para cada carácter adicional de la cadena de entrada. De hecho, si se utiliza la vuelta atrás (backtracking) en exceso, es fácil crear el equivalente programático de un bucle sin fin si la entrada casi coincide con el patrón de la expresión regular. El motor de expresiones regulares puede tardar horas o incluso días en procesar una cadena de entrada relativamente corta.

A menudo, las aplicaciones sufren una reducción del rendimiento por usar la vuelta atrás (backtracking), a pesar de que esta no es esencial para una coincidencia. Por ejemplo, la expresión \b\p{Lu}\w*\b regular coincide con todas las palabras que comienzan con un carácter en mayúsculas, como se muestra en la tabla siguiente:

Modelo Descripción
\b Iniciar la búsqueda de coincidencias en un límite de palabras.
\p{Lu} Busca una coincidencia con un carácter en mayúsculas.
\w* Busca una coincidencia con cero o más caracteres alfabéticos.
\b Finalizar la búsqueda de coincidencias en un límite de palabras.

Dado que un límite de palabras no es lo mismo que, ni un subconjunto de, un carácter de palabra, no existe la posibilidad de que el motor de expresiones regulares cruce un límite de palabras cuando trate de coincidir con caracteres de palabra. Por lo tanto, para esta expresión regular, la vuelta atrás (backtracking) nunca puede contribuir al éxito general de ninguna coincidencia. Solo puede degradar el rendimiento porque el motor de expresiones regulares se ve obligado a guardar su estado para cada coincidencia preliminar correcta de un carácter alfabético.

Si determina que el retroceso no es necesario, puede deshabilitarlo de dos maneras:

  • Al establecer la RegexOptions.NonBacktracking opción (introducida en .NET 7). Para obtener más información, consulte modo sin retroceso.

  • Mediante el uso del (?>subexpression) elemento de lenguaje, conocido como un grupo atómico. En el ejemplo siguiente se analiza una cadena de entrada mediante dos expresiones regulares. La primera, \b\p{Lu}\w*\b, se basa en el retroceso. La segunda, \b\p{Lu}(?>\w*)\b, deshabilita el retroceso. Como se muestra en la salida del ejemplo, ambos generan el mismo 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
    

En muchos casos, el retroceso es esencial para hacer coincidir un patrón de expresión regular con el texto de entrada. Sin embargo, el retroceso excesivo puede degradar gravemente el rendimiento y crear la impresión de que una aplicación ha dejado de responder. En concreto, este problema surge cuando los cuantificadores están anidados y el texto que coincide con la subexpresión externa es un subconjunto del texto que coincide con la subexpresión interna.

Advertencia

Además de evitar un retroceso excesivo, debe usar la característica de tiempo de espera para asegurarse de que el retroceso excesivo no degrada gravemente el rendimiento de las expresiones regulares. Para obtener más información, consulte la sección Uso de valores de tiempo de espera .

Por ejemplo, el patrón de expresión regular ^[0-9A-Z]([-.\w]*[0-9A-Z])*\$$ está diseñado para buscar coincidencias con un número de pieza que contiene al menos un carácter alfanumérico. Cualquier carácter adicional puede constar de un carácter alfanumérico, un guión, un carácter de subrayado o un punto, aunque el último carácter debe ser alfanumérico. Un signo de dólar indica el final del número de pieza. En algunos casos, este patrón de expresión regular puede mostrar un rendimiento deficiente porque los cuantificadores están anidados y porque la subexpresión [0-9A-Z] es un subconjunto de la subexpresión [-.\w]*.

En estos casos, puede optimizar el rendimiento de la expresión regular quitando los cuantificadores anidados y reemplazando la subexpresión externa con una aserción de búsqueda anticipada o de búsqueda tardía de ancho cero. Las aserciones de búsqueda anticipada y de búsqueda tardía son delimitadores. No mueven el puntero en la cadena de entrada, sino que miran hacia delante o detrás para comprobar si se cumple una condición especificada. Por ejemplo, la expresión regular de número de pieza se puede volver a escribir como ^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$. Este patrón de expresión regular se define como se muestra en la tabla siguiente:

Modelo Descripción
^ Iniciar la búsqueda de coincidencias con el principio de la cadena de entrada.
[0-9A-Z] Encuentra un carácter alfanumérico. El número de pieza debe constar al menos de este carácter.
[-.\w]* Buscar coincidencias con cero o más apariciones de cualquier carácter alfabético, guión o punto.
\$ Buscar coincidencias con un signo de dólar.
(?<=[0-9A-Z]) Mire detrás del signo final del dólar para asegurarse de que el carácter anterior es alfanumérico.
$ Finalizar la búsqueda de coincidencias al final de la cadena de entrada.

En el ejemplo siguiente se muestra el uso de esta expresión regular para que coincida con una matriz que contenga posibles números de parte:

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.

El lenguaje de expresiones regulares de .NET incluye los siguientes elementos de lenguaje que puede usar para eliminar cuantificadores anidados. Para obtener más información, vea Construcciones de agrupación.

Elemento Language Descripción
(?= subexpression ) Búsqueda anticipada positiva de ancho cero. Busca con anticipación la posición actual para determinar si subexpression coincide con la cadena de entrada.
(?! subexpression ) Búsqueda anticipada negativa de ancho cero. Examina con antelación la posición actual para determinar si subexpression no coincide con la cadena de entrada.
(?<= subexpression ) Búsqueda tardía positiva de ancho cero. Examina la posición previa para determinar si subexpression coincide con la cadena de entrada.
(?<! subexpression ) Búsqueda tardía negativa de ancho cero. Realiza una búsqueda tardía de la posición actual para determinar si subexpression no coincide con la cadena de entrada.

Utilice valores de tiempo de espera

Si sus expresiones regulares procesan datos de entrada que prácticamente coinciden con el patrón de expresiones regulares, normalmente se puede usar el retroceso excesivo, que afecta enormemente al rendimiento. Además de analizar cuidadosamente el uso de la vuelta atrás (backtracking) y probar la expresión regular en entradas casi coincidentes, debe establecer siempre un valor de tiempo de espera para minimizar el efecto de la excesiva vuelta atrás (backtracking), en caso de que se produzca.

El intervalo de tiempo de espera para expresiones regulares define el período durante el cual el motor de búsqueda de expresiones regulares intentará encontrar una coincidencia antes de que se agote el tiempo de espera. Dependiendo del patrón de la expresión regular y el texto de entrada, el tiempo de ejecución podría exceder el intervalo de tiempo de espera especificado, pero no dedicará más tiempo al retroceso que el intervalo de tiempo de espera especificado. El intervalo de tiempo de espera predeterminado es Regex.InfiniteMatchTimeout, lo que significa que la expresión regular no agotará el tiempo de espera. Puede invalidar este valor y definir un intervalo de tiempo de espera como se indica a continuación:

Si ha definido un intervalo de tiempo de espera y no se encuentra una coincidencia al final de ese intervalo, el método de expresión regular produce una RegexMatchTimeoutException excepción. En el controlador de excepciones, puede optar por volver a intentar la coincidencia con un intervalo de tiempo de espera más largo, abandonar el intento de coincidencia y suponer que no hay ninguna coincidencia, o abandonar el intento de coincidencia y registrar la información de excepción para el análisis futuro.

En el ejemplo siguiente se define un GetWordData método que crea una instancia de una expresión regular con un intervalo de tiempo de espera de 350 milisegundos para calcular el número de palabras y el número medio de caracteres de una palabra de un documento de texto. Si se agota el tiempo de espera en la operación de coincidencia, el intervalo de tiempo de espera se incrementa en 350 milisegundos y el objeto Regex se reinicializa. Si el nuevo intervalo de tiempo de espera es mayor de 1 segundo, el método vuelve a producir la excepción y se la envía al autor de la llamada.

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:               {info.Item1:N0}");
            Console.WriteLine($"Average Word Length: {info.Item2:N2} characters");
        }
        catch (IOException e)
        {
            Console.WriteLine($"IOException reading file '{title}'");
            Console.WriteLine(e.Message);
        }
        catch (RegexMatchTimeoutException e)
        {
            Console.WriteLine($"The operation timed out after {e.MatchTimeout.TotalMilliseconds:N0} milliseconds");
        }
    }
}

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

Capturar solo cuando sea necesario

Las expresiones regulares de .NET admiten construcciones de agrupación, que permiten agrupar un patrón de expresión regular en una o varias subexpresiones. Las construcciones de agrupación más usadas en el lenguaje de expresiones regulares de .NET son (subexpresión, que define un grupo de captura numerado y )(?<> de nombres), que define un grupo de captura con nombre. Las construcciones de agrupación son esenciales para crear referencias inversas y para definir una subexpresión a la que se aplica un cuantificador.

Sin embargo, el uso de estos elementos de lenguaje tiene un costo. Hacen que el objeto GroupCollection devuelto por la propiedad Match.Groups se rellene con las capturas con nombre o sin nombre más recientes. Si una única construcción de agrupación ha capturado varias subcadenas en la cadena de entrada, también rellenan el objeto CaptureCollection devuelto por la propiedad Group.Captures de un grupo de captura determinado con varios objetos Capture.

A menudo, las construcciones de agrupación se usan en una expresión regular solo para que los cuantificadores se puedan aplicar a ellos. Los grupos capturados por estas subexpresiones no se usan más adelante. Por ejemplo, la expresión \b(\w+[;,]?\s?)+[.?!] regular está diseñada para capturar una oración completa. En la tabla siguiente se describen los elementos de lenguaje de este patrón de expresión regular y su efecto en las colecciones Match y Match.Groups del objeto Group.Captures.

Modelo Descripción
\b Iniciar la búsqueda de coincidencias en un límite de palabras.
\w+ Busca coincidencias con uno o más caracteres alfabéticos.
[;,]? Busca coincidencias con cero o una coma o un punto y coma.
\s? Busca coincidencias con cero o un carácter de espacio en blanco.
(\w+[;,]?\s?)+ Busca coincidencias con una o más apariciones de uno o más caracteres alfabéticos seguidos de una coma o un punto y coma opcional, seguido de un carácter opcional de espacio en blanco. Este patrón define el primer grupo de captura, que es necesario para que la combinación de varios caracteres de palabras (es decir, una palabra) seguida de un símbolo de puntuación opcional se repetirá hasta que el motor de expresiones regulares llegue al final de una oración.
[.?!] Busca coincidencias con un punto, un signo de interrogación o un signo de exclamación.

Como se muestra en el ejemplo siguiente, cuando se encuentra una coincidencia, los objetos GroupCollection y CaptureCollection se rellenan con capturas de la coincidencia. En este caso, el grupo (\w+[;,]?\s?) de captura existe para que el + cuantificador se pueda aplicar a él, lo que permite que el patrón de expresión regular coincida con cada palabra de una oración. De lo contrario, coincidiría con la última palabra de una frase.

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: '{match.Value}' at index {match.Index}.");
            int grpCtr = 0;
            foreach (Group grp in match.Groups)
            {
                Console.WriteLine($"   Group {grpCtr}: '{grp.Value}' at index {grp.Index}.");
                int capCtr = 0;
                foreach (Capture cap in grp.Captures)
                {
                    Console.WriteLine($"      Capture {capCtr}: '{cap.Value}' at {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.

Cuando usa subexpresiones solo para aplicar cuantificadores a ellos y no está interesado en el texto capturado, debe deshabilitar las capturas de grupo. Por ejemplo, el (?:subexpression) elemento de lenguaje impide que el grupo al que se aplique capture subcadenas coincidentes. En el ejemplo siguiente, el patrón de expresión regular del ejemplo anterior se cambia a \b(?:\w+[;,]?\s?)+[.?!]. Como se muestra en la salida, impide que el motor de expresiones regulares rellene las colecciones GroupCollection y 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: '{match.Value}' at index {match.Index}.");
            int grpCtr = 0;
            foreach (Group grp in match.Groups)
            {
                Console.WriteLine($"   Group {grpCtr}: '{grp.Value}' at index {grp.Index}.");
                int capCtr = 0;
                foreach (Capture cap in grp.Captures)
                {
                    Console.WriteLine($"      Capture {capCtr}: '{cap.Value}' at {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.

Puede deshabilitar las capturas de una de las maneras siguientes:

  • Utilice el (?:subexpression) elemento de idioma. Este elemento impide la captura de subcadenas coincidentes en el grupo al que se aplica. No deshabilita las capturas de subcadenas en ningún grupo anidado.

  • Use la opción ExplicitCapture. Inactiva todas las capturas anónimas o implícitas en el patrón de expresión regular. Cuando se usa esta opción, solo se pueden capturar subcadenas que coincidan con grupos con nombre definidos con el (?<name>subexpression) elemento de lenguaje. La ExplicitCapture marca se puede pasar al options parámetro de un Regex constructor de clase o al options parámetro de un Regex método de coincidencia estático.

  • Usa la opción n en el elemento de idioma (?imnsx). Esta opción deshabilita todas las capturas sin nombre o implícitas desde el punto del patrón de expresión regular en el que aparece el elemento. Las capturas se deshabilitan hasta el final del patrón o hasta que la (-n) opción habilita capturas sin nombre o implícitas. Para obtener más información, vea Construcciones varias.

  • Usa la opción n en el elemento de idioma (?imnsx:subexpression). Esta opción deshabilita todas las capturas sin nombre o implícitas en subexpression. Las capturas por grupos de captura anidados sin nombre o implícitos también se deshabilitan.

Seguridad de hilos

La clase Regex es en sí misma segura para subprocesos e inmutable (de solo lectura). Es decir, Regex los objetos se pueden crear en cualquier subproceso y compartirse entre subprocesos; se puede llamar a métodos coincidentes desde cualquier subproceso y nunca modificar ningún estado global.

Sin embargo, los objetos de resultado (Match y MatchCollection) devueltos por Regex deben usarse en un único subproceso. Aunque muchos de estos objetos son inmutables lógicamente, sus implementaciones podrían retrasar el cálculo de algunos resultados para mejorar el rendimiento y, como resultado, los autores de llamadas deben serializar el acceso a ellos.

Si es necesario compartir objetos de resultado de Regex en varios subprocesos, estos objetos se pueden convertir en instancias seguras para subprocesos llamando a sus métodos sincronizados. Con la excepción de los enumeradores, todas las clases de expresiones regulares son seguras para subprocesos o se pueden convertir en objetos seguros para subprocesos mediante un método sincronizado.

Los enumeradores son la única excepción. Debe serializar las llamadas a enumeradores de colecciones. La regla es que si una colección se puede enumerar simultáneamente en más de un subproceso, debería sincronizar los métodos del enumerador en el objeto raíz de la colección que este recorre.

Título Descripción
Detalles del comportamiento de expresiones regulares Examina la implementación del motor de expresiones regulares en .NET. El artículo se centra en la flexibilidad de las expresiones regulares y explica la responsabilidad del desarrollador para garantizar el funcionamiento eficaz y sólido del motor de expresiones regulares.
Retroceso Explica qué es el retroceso y cómo afecta al rendimiento de expresiones regulares y examina los elementos del lenguaje que proporcionan alternativas al retroceso.
Lenguaje de expresiones regulares: referencia rápida Describe los elementos del lenguaje de expresiones regulares en .NET y proporciona vínculos a documentación detallada para cada elemento de lenguaje.