Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Un'espressione regolare, o regex, è una stringa che consente allo sviluppatore di esprimere un criterio di ricerca, rendendola un modo comune per cercare testo ed estrarre risultati come sottoinsieme della stringa ricercata. In .NET, lo System.Text.RegularExpressions spazio dei nomi viene usato per definire istanze Regex e metodi statici, nonchè per stabilire corrispondenze con modelli definiti dall'utente. In questo articolo, si apprenderà in che modo usare la generazione di origine per generare istanze Regex e ottimizzare le prestazioni.
Nota
Dove possibile, usare le espressioni regolari generate dall'origine anziché compilare espressioni regolari usando l'opzione RegexOptions.Compiled. La generazione del codice sorgente consente all'app di avviarsi più rapidamente, di funzionare più velocemente e di essere più facilmente ottimizzabile. Per sapere quando è possibile la generazione del codice sorgente, vedere Quando usarlo.
Espressioni regolari compilate
Quando si scrive new Regex("somepattern"), si verificano alcune cose. Il pattern specificato viene analizzato, sia per assicurarne la validità sia per trasformarlo in un albero interno che rappresenta la regex analizzata. L'albero viene quindi ottimizzato in modi diversi, in modo da trasformare il criterio in una variante funzionalmente equivalente che può essere eseguita in modo più efficiente. L'albero viene scritto in una forma che può essere interpretata come una serie di codici operativi e operandi che forniscono istruzioni al motore di interpretazione regex su come corrispondere. Quando viene eseguita una corrispondenza, l'interprete si limita a scorrere attraverso le istruzioni, elaborandole in rapporto al testo di input. Quando si crea una nuova istanza Regex o si chiama uno dei metodi statici in Regex, l'interprete è il motore predefinito in uso.
Quando si specifica RegexOptions.Compiled, si eseguono tutte le stesse operazioni in fase di costruzione. Le istruzioni risultanti vengono ulteriormente trasformate dal compilatore basato su reflection-emit in istruzioni del linguaggio intermedio che vengono scritte in pochi oggetti DynamicMethod. Quando viene eseguita una corrispondenza, i metodi DynamicMethod vengono richiamati. Questo linguaggio intermedio (IL) fa esattamente ciò che farebbe l'interprete, ma è specializzato per il pattern esatto che viene elaborato. Ad esempio, se il criterio contiene [ac], l'interprete visualizzerebbe un codice operativo che dice "confrontare il carattere di input nella posizione corrente con il set specificato in questa descrizione del set". Mentre il linguaggio intermedio compilato conterrebbe un codice che dice effettivamente "corrispondere al carattere di input nella posizione corrente rispetto a 'a' o 'c'". Questa gestione dei casi speciali e la capacità di eseguire ottimizzazioni in base alla conoscenza dello schema sono alcune delle ragioni principali per cui la specifica RegexOptions.Compiled produce una velocità effettiva di corrispondenza molto più alta rispetto a quella dell'interprete.
Il RegexOptions.Compiled presenta diversi svantaggi. La più significativa è che la sua costruzione è costosa. Non solo si devono sostenere tutti gli stessi costi dell'interprete, ma è necessario compilare l'albero RegexNode risultante e i codici operativi/operandi generati con il linguaggio intermedio, il che aggiunge spese non trascurabili. IL generato deve inoltre essere compilato in modalità JIT al primo utilizzo, il che comporta spese ancora maggiori all'avvio.
RegexOptions.Compiled rappresenta un compromesso fondamentale tra i costi generali del primo utilizzo e quelli di ogni utilizzo successivo. L'uso di System.Reflection.Emit impedisce anche l'uso di RegexOptions.Compiled in determinati ambienti. Alcuni sistemi operativi non consentono l'esecuzione di codice generato dinamicamente e pertanto, in tali sistemi, Compiled diventa non operativo.
Generazione del codice sorgente
.NET 7 ha introdotto un nuovo generatore di origine RegexGenerator. Un generatore di origine è un componente che si collega al compilatore e aumenta l'unità di compilazione con codice sorgente aggiuntivo. .NET SDK include un generatore di origine che riconosce l'attributo GeneratedRegexAttribute su un metodo parziale che restituisce Regex. A partire da .NET 9, l'attributo può essere applicato anche alle proprietà parziali. Il generatore di origine fornisce un'implementazione di tale metodo o proprietà che contiene tutta la logica per .Regex Ad esempio, è possibile che in precedenza sia stato scritto codice simile al seguente:
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
}
}
Per usare il generatore di origine, riscrivere il codice precedente come segue:
[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 partire da .NET 9, è anche possibile applicare GeneratedRegexAttribute a una proprietà parziale anziché a un metodo parziale. Questa funzionalità è abilitata dal supporto di C# 13 per le proprietà parziali. L'esempio seguente illustra l'equivalente di una proprietà:
[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
}
}
Suggerimento
Il flag RegexOptions.Compiled viene ignorato dal generatore di origine, quindi non è necessario nella versione generata dall'origine.
L'implementazione generata di AbcOrDefGeneratedRegex()memorizza nella cache un'istanza singleton in modo analogoRegex, pertanto non è necessaria alcuna memorizzazione nella cache aggiuntiva per poter usare il codice.
L'immagine seguente è un'acquisizione dello schermo dell'istanza della cache generata dal generatore sorgente, internal alla sottoclasse Regex che il generatore sorgente emette:
Ma come si può vedere, non si tratta solo di fare new Regex(...). Il generatore di origine invece emette come codice C# una implementazione personalizzata basata su una Regex, con logica simile a quella generata da RegexOptions.Compiled in codice IL. Si ottengono tutti i vantaggi in termini di prestazioni e velocità effettiva di RegexOptions.Compiled (anzi, di più) e di avvio di Regex.CompileToAssembly, ma senza la complessità di CompileToAssembly. L'origine generata fa parte del progetto, il che significa che è anche facilmente visualizzabile e sottoponibile a debug.
Suggerimento
In Visual Studio fare clic con il pulsante destro del mouse sulla dichiarazione parziale di metodo o proprietà e scegliere Vai a definizione. In alternativa, selezionare il nodo del progetto in Esplora soluzioni, quindi espandere Dependencies>Analyzers>System.Text.RegularExpressions.Generator>System.Text.RegularExpressions.Generator.RegexGenerator>RegexGenerator.g.cs per visualizzare il codice C# generato da questo generatore di regex.
È possibile impostarvi punti di interruzione, eseguire dei passaggi e usarlo come strumento di apprendimento per comprendere esattamente come il motore della regex elabora il modello tramite l'input. II generatore produce anche commenti indicati da barre triple (XML) per rendere l'espressione comprensibile a colpo d'occhio e nei casi in cui viene usata.
All'interno dei file generati dal codice sorgente
Con .NET 7, sia il generatore di origine che il RegexCompiler sono stati quasi interamente riscritti, modificando radicalmente la struttura del codice generato. Questo approccio è stato esteso alla gestione di tutti i costrutti, con un'unica eccezione, e sia RegexCompiler che il generatore di origine continuano a mappare sostanzialmente 1:1 tra loro, seguendo il nuovo approccio. Si consideri l'output del generatore di origine per una delle funzioni principali dell'espressione 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;
}
L'obiettivo del codice generato dall’origine è quello di essere comprensibile, dotato di una struttura facile da seguire, di commenti che spieghino cosa viene fatto in ogni passaggio e, in generale, di codice generato in base al principio guida secondo cui il generatore dovrebbe emettere codice come se fosse stato scritto da un essere umano. Anche quando è coinvolto il backtracking, questo si integra nella struttura del codice, anziché utilizzare uno stack per determinare il prossimo salto. Ad esempio, di seguito è riportato il codice per la stessa funzione di corrispondenza generata quando l'espressione è [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;
}
È possibile visualizzare la struttura del backtracking nel codice, con un'etichetta CharLoopBacktrack generata per indicare il punto in cui eseguire il backtrack e un'etichetta goto utilizzata per saltare a quella posizione quando una sezione successiva dell'espressione regolare fallisce.
Se si esamina l'implementazione del codice RegexCompiler e il generatore di origine, si noterà che sono estremamente simili, ovvero metodi denominati in modo simile, con una struttura di chiamata simile e anche commenti simili in tutta l'implementazione. Nella maggior parte dei casi, generano codice identico, anche se in un caso viene espresso in IL e nell'altro in C#. Naturalmente, il compilatore C# è quindi responsabile della traduzione del codice C# in linguaggio intermedio, pertanto il risultato finale non sarà identico in entrambi i casi. Il generatore di origine si basa su questa funzione in vari casi, traendo vantaggio dal fatto che il compilatore C# ottimizzerà ulteriormente diversi costrutti di C#. Esistono pertanto alcune funzioni specifiche che consentono al generatore di origine di produrre un codice di corrispondenza più ottimizzato rispetto a RegexCompiler. Ad esempio, in uno degli esempi precedenti, è possibile visualizzare il generatore di origine che emette un'istruzione switch, con un ramo per 'a' e un altro ramo per 'b'. Poiché il compilatore C# è molto abile nell'ottimizzare le istruzioni switch, avendo a disposizione più strategie per farlo in modo efficiente, il generatore di codice ha una capacità di ottimizzazione speciale di cui è privo RegexCompiler. In caso di alternanze, il generatore di codice sorgente esamina tutti i rami e, se è in grado di dimostrare che ogni ramo presenta un carattere iniziale diverso, emette un'istruzione switch per quel carattere ed evita di generare codice di backtracking per quell'alternanza.
Di seguito è riportato un esempio leggermente più complicato di questo. Le alternanze vengono analizzate in modo più approfondito per determinare se è possibile effettuarne il refactoring in modo da renderle più facilmente ottimizzabili dai motori di backtracking e ottenere codice generato dall'origine più semplice. Un'ottimizzazione del genere supporta l'estrazione di prefissi comuni dai rami e, se l'alternanza è atomica e l'ordinamento non è importante, riordinare i rami per consentire un'ulteriore estrazione. È possibile osservarne l'impatto nel caso del criterio Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday relativo al giorno feriale seguente, che produce una funzione di corrispondenza simile a quella indicata di seguito:
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;
}
Allo stesso tempo, il generatore di origine presenta altri problemi che normalmente non si verificano quando si esegue l'output direttamente nel linguaggio intermedio. Se si guarda a un paio di esempi di codice, è possibile notare alcune parentesi graffe commentate in modo un po' strano. Non si tratta di un errore. Il generatore di origine riconosce che, se tali parentesi graffe non sono state commentate, la struttura del backtracking si basa sul passaggio dall'esterno dell'ambito a un'etichetta definita all'interno di tale ambito; tuttavia, un'etichetta del genere non sarebbe visibile a tale goto e il codice non verrebbe compilato. Pertanto, il generatore di sorgente deve evitare che ci sia un ambito d'intralcio. In alcuni casi, si limiterà a commentare l'ambito come è stato fatto qui. In altri casi in cui ciò non è possibile, si può talvolta evitare di usare i costrutti che richiedono ambiti, ad esempio un blocco if con più istruzioni, quando farlo diventa problematico.
Il generatore di origine gestisce tutti gli handle RegexCompiler, con un'unica eccezione. Come per la gestione di RegexOptions.IgnoreCase, le implementazioni ora usano una tabella per l’utilizzo di maiuscole e minuscole per generare set in fase di costruzione e il modo in cui la corrispondenza di backreference IgnoreCase deve consultare tale tabella. Questa tabella è interna a System.Text.RegularExpressions.dlle, almeno per il momento, il codice esterno a tale assembly, incluso quello generato dal generatore di origine, non vi ha accesso. Ciò rende la gestione dei backreference IgnoreCase nel generatore di origine piuttosto complicata e, pertanto, non sono supportati. Si tratta di un costrutto non supportato dal generatore di origine che è supportato da RegexCompiler. Se tenti di utilizzare un modello che ne include uno (cosa rara), il generatore di codice sorgente non emetterà un'implementazione personalizzata e eseguirà invece il fallback su un'istanza Regex standard memorizzata nella cache.
Inoltre, né RegexCompiler né il generatore di origine supporta il nuovo RegexOptions.NonBacktracking. Se si specifica RegexOptions.Compiled | RegexOptions.NonBacktracking, il flag Compiled verrà semplicemente ignorato, mentre se si specifica NonBacktracking al generatore di codice, si tornerà alla memorizzazione nella cache di un'istanza Regex normale.
Quando usarlo
Le indicazioni generali raccomandano di usare il generatore di origine, purché sia possibile. Se attualmente si usa Regex in C# con argomenti noti in fase di compilazione, e in particolare se si sta già usando RegexOptions.Compiled, poiché la regex è stata identificata come un punto critico che trarrebbe vantaggio da una velocità effettiva più elevata, è consigliabile usare il generatore di origine. Il generatore del codice sorgente offrirà i seguenti vantaggi per le vostre espressioni regolari:
- Tutti i vantaggi della capacità di elaborazione di
RegexOptions.Compiled. - I vantaggi iniziali di non dover eseguire tutto il parsing delle espressioni regolari, l'analisi e la compilazione in fase di esecuzione.
- L'opzione di utilizzo della compilazione anticipata con il codice generato per la regex.
- Migliore facilità di debugging e comprensione del regex.
- Possibilità di ridurre le dimensioni dell'app tagliata eliminando ampie porzioni di codice associato a
RegexCompilere, potenzialmente, anche la reflection emit stessa.
Se usata con un'opzione come RegexOptions.NonBacktracking per cui il generatore di origine non può generare un'implementazione personalizzata, emetterà comunque commenti di memorizzazione nella cache e XML che descrivono l'implementazione, rendendola utile. Lo svantaggio principale del generatore di origine consiste nel fatto che genera codice aggiuntivo nell'assembly, per cui è possibile che le dimensioni aumentino. Maggiori sono il numero e la dimensione delle espressioni regolari presenti nell'app, maggiore sarà la quantità di codice che verrà per le stesse generato. In alcune situazioni, proprio come RegexOptions.Compiled potrebbe non essere necessario, così come potrebbe esserlo anche il generatore di origine. Ad esempio, se si dispone di un'espressione regolare che è necessaria solo raramente e per la quale la velocità effettiva non è rilevante, potrebbe essere più utile affidarsi all'interprete solo per l'uso sporadico.
Importante
.NET 7 include un analizzatore che identifica l'uso di Regex che può essere convertito nel generatore di origine e una correzione che esegue automaticamente la conversione: