Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Una expresión regular, o regex, es una cadena que permite a un desarrollador expresar un patrón que se busca, lo que hace que sea una forma común de buscar texto y extraer resultados como un subconjunto de la cadena buscada. En .NET, el espacio de nombres System.Text.RegularExpressions se usa para definir instancias de Regex y métodos estáticos, y buscar coincidencias con los patrones definidos por el usuario. En este artículo, aprenderá a usar la generación de origen para generar instancias de Regex para optimizar el rendimiento.
Nota:
Siempre que sea posible, use expresiones regulares generadas por código fuente en lugar de compilar expresiones regulares mediante la opción RegexOptions.Compiled. La generación de código fuente puede ayudar a que la aplicación se inicie y se ejecute más rápidamente, y se pueda reducir más. Para obtener información sobre cuándo es posible la generación de código fuente, vea Cuándo usarlo.
Expresiones regulares compiladas
Al escribir new Regex("somepattern"), suceden algunas cosas. El patrón especificado se analiza, tanto para garantizar la validez del patrón como para transformarlo en un árbol interno que representa la expresión regular analizada. A continuación, el árbol se optimiza de varias maneras para transformar el patrón en una variación funcionalmente equivalente que se puede ejecutar de forma más eficaz. El árbol se escribe en un formato que se puede interpretar como una serie de códigos de operación y operandos que proporcionan instrucciones al motor del intérprete de expresiones regulares sobre cómo realizar las coincidencias. Cuando se realiza una coincidencia, el intérprete simplemente recorre esas instrucciones y las procesa con respecto al texto de entrada. Al instanciar Regex o llamar a uno de los métodos estáticos de Regex, el intérprete es el motor predeterminado utilizado.
Al especificar RegexOptions.Compiled, se realiza todo el mismo trabajo en tiempo de construcción. Las instrucciones resultantes se transforman aún más por el compilador basado en reflexión-emisión en instrucciones de IL que se escriben en unos cuantos objetos DynamicMethod. Cuando se realiza una coincidencia, se invocan esos métodos DynamicMethod. Este IL realiza esencialmente lo que haría el intérprete, excepto que se especializa en el patrón exacto que se está procesando. Por ejemplo, si el patrón contiene [ac], el intérprete identificaría un opcode que indica "comparar el carácter de entrada en la posición actual con el conjunto específico detallado en esta descripción del conjunto". Mientras que el IL compilado contendrá código que efectivamente dice: "compara el carácter de entrada en la posición actual con 'a' o 'c'". Este caso especial y la capacidad de realizar optimizaciones basadas en el conocimiento del patrón son algunas de las principales razones por las que especificar RegexOptions.Compiled produce un rendimiento de coincidencia mucho más rápido que el intérprete.
Hay varias desventajas asociadas con RegexOptions.Compiled. Lo más impactante es que su construcción es costosa. No solo se pagan los mismos costos que para el intérprete, sino que, luego, se debe compilar ese árbol RegexNode resultante y los códigos de operación y operandos generados en el lenguaje intermedio, lo que suma gastos que no se pueden menospreciar. El IL generado necesita además ser compilado con JIT en su primer uso, lo que da lugar a un gasto aún mayor al principio.
RegexOptions.Compiled representa un equilibrio fundamental entre las sobrecargas en el primer uso y las sobrecargas en cada uso posterior. El uso de System.Reflection.Emit también impide el uso de RegexOptions.Compiled en determinados entornos; algunos sistemas operativos no permiten ejecutar el código generado dinámicamente y, en estos sistemas, Compiled no tiene efecto.
Generación de origen
.NET 7 introdujo un nuevo generador de origen RegexGenerator. Un generador de código fuente es un componente que se conecta al compilador y aumenta la unidad de compilación con código fuente adicional. El SDK de .NET incluye un generador de origen que reconoce el GeneratedRegexAttribute atributo en un método parcial que devuelve Regex. A partir de .NET 9, el atributo también se puede aplicar a propiedades parciales. El generador de origen proporciona una implementación de ese método o propiedad que contiene toda la lógica de .Regex Por ejemplo, es posible que ya haya escrito código como este:
private static readonly Regex s_abcOrDefGeneratedRegex =
new(pattern: "abc|def",
options: RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static void EvaluateText(string text)
{
if (s_abcOrDefGeneratedRegex.IsMatch(text))
{
// Take action with matching text
}
}
Para usar el generador de código fuente, vuelva a escribir el código anterior de la siguiente manera:
[GeneratedRegex("abc|def", RegexOptions.IgnoreCase, "en-US")]
private static partial Regex AbcOrDefGeneratedRegex();
private static void EvaluateText(string text)
{
if (AbcOrDefGeneratedRegex().IsMatch(text))
{
// Take action with matching text
}
}
A partir de .NET 9, también puede aplicar el GeneratedRegexAttribute a una propiedad parcial en lugar de un método parcial. Esto está habilitado por la compatibilidad de C# 13 con propiedades parciales. En el ejemplo siguiente se muestra la propiedad equivalente:
[GeneratedRegex("abc|def", RegexOptions.IgnoreCase, "en-US")]
private static partial Regex AbcOrDefGeneratedRegexProperty { get; }
private static void EvaluateText(string text)
{
if (AbcOrDefGeneratedRegexProperty.IsMatch(text))
{
// Take action with matching text
}
}
Sugerencia
El generador de código fuente omite la marca RegexOptions.Compiled, por lo que ya no es necesaria en la versión generada de origen.
La implementación generada de AbcOrDefGeneratedRegex() almacena en caché de forma similar una instancia singleton Regex, por lo que no se necesita almacenamiento en caché adicional para consumir código.
La siguiente imagen es una captura de pantalla de la instancia almacenada en caché generada por el origen, internal a la subclase Regex que emite el generador de origen:
Pero, como se puede ver, no solo está haciendo new Regex(...). En su lugar, el generador de código fuente emite en código de C# una implementación derivada de Regex personalizada con una lógica similar a la que RegexOptions.Compiled emite en IL. 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.
Sugerencia
En Visual Studio, haga clic con el botón derecho en la declaración parcial de método o propiedad y seleccione Ir a definición. También puede seleccionar el nodo del proyecto en Explorador de soluciones y, luego, expandir Dependencies>Analizadores>System.Text.RegularExpressions.Generator>System.Text.RegularExpressions.Generator.RegexGenerator>RegexGenerator.g.cs para ver el código de C# generado desde este generador regex.
Puede establecer puntos de interrupción, recorrerlo paso a paso y usarlo como una herramienta de aprendizaje para comprender exactamente cómo el motor de regex procesa tu patrón con la entrada. El generador incluso genera comentarios de barra diagonal triple (XML) para ayudar a comprender la expresión de un vistazo y dónde se usa.
Dentro de los archivos generados por el código fuente
Con .NET 7, tanto el generador de código fuente como RegexCompiler se reescribieron casi por completo, lo que cambió fundamentalmente la estructura del código generado. Este enfoque se ha ampliado para gestionar todas las construcciones (con una salvedad), y tanto RegexCompiler como el generador de código siguen asignándose en una relación prácticamente 1:1, siguiendo el nuevo enfoque. Considere la salida del generador de código fuente para una de las funciones principales de la expresión abc|def:
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
int pos = base.runtextpos;
int matchStart = pos;
ReadOnlySpan<char> slice = inputSpan.Slice(pos);
// Match with 2 alternative expressions, atomically.
{
if (slice.IsEmpty)
{
return false; // The input didn't match.
}
switch (slice[0])
{
case 'A' or 'a':
if ((uint)slice.Length < 3 ||
!slice.Slice(1).StartsWith("bc", StringComparison.OrdinalIgnoreCase)) // Match the string "bc" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 3;
slice = inputSpan.Slice(pos);
break;
case 'D' or 'd':
if ((uint)slice.Length < 3 ||
!slice.Slice(1).StartsWith("ef", StringComparison.OrdinalIgnoreCase)) // Match the string "ef" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 3;
slice = inputSpan.Slice(pos);
break;
default:
return false; // The input didn't match.
}
}
// The input matched.
base.runtextpos = pos;
base.Capture(0, matchStart, pos);
return true;
}
El objetivo del código generado por el código fuente es ser comprensible, con una estructura fácil de seguir, con comentarios que expliquen lo que se hace en cada paso y, en general, con código emitido bajo el principio rector de que el generador debe emitir código como si lo hubiera escrito un humano. Incluso cuando la vuelta atrás (backtracking) está implicada, la estructura de esta característica se convierte en parte de la estructura del código, en lugar de depender de una pila para indicar dónde saltar a continuación. Por ejemplo, este es el código de la misma función de coincidencia generada cuando la expresión es [ab]*[bc]:
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
int pos = base.runtextpos;
int matchStart = pos;
int charloop_starting_pos = 0, charloop_ending_pos = 0;
ReadOnlySpan<char> slice = inputSpan.Slice(pos);
// Match a character in the set [ABab] greedily any number of times.
//{
charloop_starting_pos = pos;
int iteration = slice.IndexOfAnyExcept(Utilities.s_ascii_600000006000000);
if (iteration < 0)
{
iteration = slice.Length;
}
slice = slice.Slice(iteration);
pos += iteration;
charloop_ending_pos = pos;
goto CharLoopEnd;
CharLoopBacktrack:
if (Utilities.s_hasTimeout)
{
base.CheckTimeout();
}
if (charloop_starting_pos >= charloop_ending_pos ||
(charloop_ending_pos = inputSpan.Slice(charloop_starting_pos, charloop_ending_pos - charloop_starting_pos).LastIndexOfAny(Utilities.s_ascii_C0000000C000000)) < 0)
{
return false; // The input didn't match.
}
charloop_ending_pos += charloop_starting_pos;
pos = charloop_ending_pos;
slice = inputSpan.Slice(pos);
CharLoopEnd:
//}
// Advance the next matching position.
if (base.runtextpos < pos)
{
base.runtextpos = pos;
}
// Match a character in the set [BCbc].
if (slice.IsEmpty || ((uint)((slice[0] | 0x20) - 'b') > (uint)('c' - 'b')))
{
goto CharLoopBacktrack;
}
// The input matched.
pos++;
base.runtextpos = pos;
base.Capture(0, matchStart, pos);
return true;
}
Puede ver la estructura de la vuelta atrás (backtracking) en el código, con una etiqueta CharLoopBacktrack emitida para indicar dónde volver atrás y una etiqueta goto usada para saltar a esa ubicación cuando falla una parte subsecuente de la expresión regular.
Si observa el código que implementa RegexCompiler y el generador de código fuente, su aspecto será muy similar: métodos con nombre similar, estructura de llamadas similar e incluso comentarios similares a lo largo de la implementación. En su mayor parte, dan como resultado código idéntico, aunque uno en IL y el otro en C#. Por supuesto, el compilador de C# es responsable de convertir C# en IL, por lo que es probable que el IL resultante en ambos casos no sea idéntico. El generador de código fuente depende de ello en varios casos, y aprovecha el hecho de que el compilador de C# optimizará aún más varias construcciones de C#. Hay algunas cosas específicas por las que el generador de código fuente producirá un código coincidente más optimizado que RegexCompiler. Por ejemplo, en uno de los ejemplos anteriores, puede ver que el generador de código fuente emite una sentencia switch, con una rama para 'a' y otra rama para 'b'. Debido a que el compilador de C# es muy bueno optimizando instrucciones switch, con múltiples estrategias a su disposición para hacerlo de manera eficiente, el generador de código fuente tiene una optimización especial que RegexCompiler no tiene. En el caso de las alternancias, el generador de código fuente examina todas las ramas y, si puede demostrar que cada rama comienza con un carácter inicial diferente, emitirá una instrucción switch sobre ese primer carácter y evitará generar cualquier código de vuelta atrás (backtracking) para esa alternancia.
Este es un ejemplo ligeramente más complicado de eso. Las alternancias se analizan más a fondo para determinar si es posible refactorizarlas de forma que los motores de vuelta atrás (backtracking) las optimicen más fácilmente y se genere un código fuente más sencillo. Una de estas optimizaciones permite extraer prefijos comunes de las ramas y, si la alternancia es atómica de modo que el orden no importa, reordenar las ramas para permitir más extracciones de este tipo. Puede ver el impacto de eso en el siguiente patrón semanal Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday, que produce una función de emparejamiento como esta:
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
int pos = base.runtextpos;
int matchStart = pos;
char ch;
ReadOnlySpan<char> slice = inputSpan.Slice(pos);
// Match with 6 alternative expressions, atomically.
{
int alternation_starting_pos = pos;
// Branch 0
{
if ((uint)slice.Length < 6 ||
!slice.StartsWith("monday", StringComparison.OrdinalIgnoreCase)) // Match the string "monday" (ordinal case-insensitive)
{
goto AlternationBranch;
}
pos += 6;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 1
{
if ((uint)slice.Length < 7 ||
!slice.StartsWith("tuesday", StringComparison.OrdinalIgnoreCase)) // Match the string "tuesday" (ordinal case-insensitive)
{
goto AlternationBranch1;
}
pos += 7;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch1:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 2
{
if ((uint)slice.Length < 9 ||
!slice.StartsWith("wednesday", StringComparison.OrdinalIgnoreCase)) // Match the string "wednesday" (ordinal case-insensitive)
{
goto AlternationBranch2;
}
pos += 9;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch2:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 3
{
if ((uint)slice.Length < 8 ||
!slice.StartsWith("thursday", StringComparison.OrdinalIgnoreCase)) // Match the string "thursday" (ordinal case-insensitive)
{
goto AlternationBranch3;
}
pos += 8;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch3:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 4
{
if ((uint)slice.Length < 6 ||
!slice.StartsWith("fr", StringComparison.OrdinalIgnoreCase) || // Match the string "fr" (ordinal case-insensitive)
((((ch = slice[2]) | 0x20) != 'i') & (ch != 'İ')) || // Match a character in the set [Ii\u0130].
!slice.Slice(3).StartsWith("day", StringComparison.OrdinalIgnoreCase)) // Match the string "day" (ordinal case-insensitive)
{
goto AlternationBranch4;
}
pos += 6;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch4:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 5
{
// Match a character in the set [Ss].
if (slice.IsEmpty || ((slice[0] | 0x20) != 's'))
{
return false; // The input didn't match.
}
// Match with 2 alternative expressions, atomically.
{
if ((uint)slice.Length < 2)
{
return false; // The input didn't match.
}
switch (slice[1])
{
case 'A' or 'a':
if ((uint)slice.Length < 8 ||
!slice.Slice(2).StartsWith("turday", StringComparison.OrdinalIgnoreCase)) // Match the string "turday" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 8;
slice = inputSpan.Slice(pos);
break;
case 'U' or 'u':
if ((uint)slice.Length < 6 ||
!slice.Slice(2).StartsWith("nday", StringComparison.OrdinalIgnoreCase)) // Match the string "nday" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 6;
slice = inputSpan.Slice(pos);
break;
default:
return false; // The input didn't match.
}
}
}
AlternationMatch:;
}
// The input matched.
base.runtextpos = pos;
base.Capture(0, matchStart, pos);
return true;
}
Al mismo tiempo, el generador de código fuente tiene que hacer frente a otros problemas que simplemente no existen cuando la salida va directamente a IL. Si examina un par de ejemplos de código anteriores, puedes ver algunas llaves comentadas de forma un tanto extraña. No se trata de un error. El generador de código fuente está reconociendo que, si esas llaves no estuvieran comentadas, la estructura de vuelta atrás (backtracking) se basaría en saltar desde fuera del ámbito a una etiqueta definida dentro de ese ámbito; tal etiqueta no sería visible para tal goto y el código daría error al compilar. Así, el generador de código debe evitar que haya un ámbito en medio. En algunos casos, simplemente se comentará el ámbito como se ha hecho aquí. En otros casos en los que no sea posible, a veces se pueden evitar construcciones que requieren ámbitos (como un bloque if de varias sentencias) si hacerlo resulta problemático.
El generador de código fuente controla todo lo que RegexCompiler controla, con una excepción. Al igual que al manejar RegexOptions.IgnoreCase, las implementaciones ahora utilizan una tabla de conversión de mayúsculas y minúsculas para generar conjuntos en el momento de la construcción, y es necesario que la coincidencia de referencia inversa de IgnoreCase consulte esa tabla. Esa tabla es interna para System.Text.RegularExpressions.dll y, por ahora, al menos, el código externo a ese ensamblado (incluido el código emitido por el generador de código fuente) no tiene acceso a ella. Esto hace que el manejo de referencias inversas de IgnoreCase sea un desafío en el generador de código y no se admiten. Esta es la única construcción no admitida por el generador de código fuente que es compatible con RegexCompiler. Si intenta usar un patrón que tiene una de estas (lo cual es poco frecuente), el generador de código fuente no emitirá una implementación personalizada y, en su lugar, revertirá al almacenamiento en caché de una instancia normal de Regex:
Además, ni RegexCompiler ni el generador de código fuente admiten el nuevo valor RegexOptions.NonBacktracking. Si especifica RegexOptions.Compiled | RegexOptions.NonBacktracking, la marca Compiled se omitirá y, si especifica NonBacktracking en el generador de código fuente, se revertirá de forma similar al almacenamiento en caché de una instancia normal de Regex.
Cuándo se debe usar
La guía general es que si puede usar el generador de código fuente, hágalo. Si usa Regex hoy en C# con argumentos conocidos en tiempo de compilación, y especialmente si ya usa RegexOptions.Compiled, porque la expresión regular se ha identificado como un punto crítico que se beneficiaría de un mayor rendimiento, debería usar el generador de código. El generador de código ofrecerá a tu regex las siguientes ventajas:
- Todas las ventajas de rendimiento de
RegexOptions.Compiled. - Las ventajas para startups de no tener que realizar todo el análisis, parseo y compilación de expresiones regulares en tiempo de ejecución.
- La opción de utilizar la compilación anticipada con el código generado para la expresión regular.
- Mejor capacidad de depuración y comprensión de la expresión regular (regex).
- La posibilidad de reducir el tamaño de su aplicación recortada mediante el recorte de grandes franjas de código asociado con
RegexCompiler(y potencialmente incluso la propia emisión de reflexión).
Cuando se usa con una opción como RegexOptions.NonBacktracking para la que el generador de código fuente no puede generar una implementación personalizada, seguirá emitiendo el almacenamiento en caché y los comentarios XML que describen la implementación, lo que la hace valiosa. La principal desventaja del generador de código fuente es que emite código adicional en el ensamblado, por lo que existe la posibilidad de que aumente el tamaño. Cuantas más expresiones regulares haya en tu aplicación y cuanto más grandes sean estas expresiones, más código se generará para ellas. En algunas situaciones, al igual que RegexOptions.Compiled puede ser innecesario, también puede serlo el generador de código fuente. Por ejemplo, si tiene una expresión regular que solo es necesaria en raras ocasiones y para la que el rendimiento no importa, podría ser más beneficioso confiar solo en el intérprete para ese uso esporádico.
Importante
.NET 7 incluye un analizador que identifica el uso de Regex que se podría convertir en el generador de código fuente, y un solucionador que realiza la conversión automáticamente: