Condividi tramite


Procedure consigliate per le espressioni regolari in .NET

Il motore delle espressioni regolari in .NET è uno strumento potente e completo che elabora il testo in base alle corrispondenze dei criteri anziché al confronto e alla corrispondenza del testo letterale. Nella maggior parte dei casi, esegue il riconoscimento di schemi in modo rapido ed efficiente. In alcuni casi, tuttavia, il motore delle espressioni regolari può sembrare lento. In casi estremi, può anche sembrare di smettere di rispondere mentre elabora un input relativamente piccolo nel corso di ore o persino giorni.

Questo articolo descrive alcune delle procedure consigliate che gli sviluppatori possono adottare per garantire che le espressioni regolari ottengano prestazioni ottimali.

Avvertimento

Quando si usa System.Text.RegularExpressions per elaborare l'input non attendibile, passare un timeout. Un utente malintenzionato può fornire input a RegularExpressions, causando un attacco di negazione del servizio . Le API del framework di ASP.NET Core che utilizzano RegularExpressions trasmettono un timeout.

Considerare l'origine delle informazioni

In generale, le espressioni regolari possono accettare due tipi di input: vincolati o non vincolati. L'input vincolato è un testo che ha origine da un'origine nota o affidabile e segue un formato predefinito. L'input non vincolato è un testo che ha origine da un'origine non affidabile, ad esempio un utente Web, e potrebbe non seguire un formato predefinito o previsto.

I modelli di espressione regolare vengono spesso scritti in modo che corrispondano a un input valido. Ovvero, gli sviluppatori esaminano il testo che vogliono abbinare e quindi scrivono un pattern di espressione regolare che corrisponde. Gli sviluppatori determinano quindi se questo modello richiede correzione o ulteriore elaborazione testandolo con più elementi di input validi. Quando il modello corrisponde a tutti gli input validi presunti, viene dichiarato pronto per la produzione e può essere incluso in un'applicazione rilasciata. Questo approccio rende uno schema di espressione regolare adatto per corrispondere a un input vincolato. Tuttavia, non lo rende adatto alla gestione di input senza restrizioni.

Per trovare una corrispondenza con l'input non vincolato, un'espressione regolare deve gestire in modo efficiente tre tipi di testo:

  • Testo che corrisponde al criterio di espressione regolare.
  • Testo che non corrisponde al criterio di espressione regolare.
  • Testo che corrisponde quasi al criterio di espressione regolare.

L'ultimo tipo di testo è particolarmente problematico per un'espressione regolare scritta per gestire l'input vincolato. Se tale espressione regolare si basa anche su backtracking esteso, il motore delle espressioni regolari può impiegare una quantità eccessiva di tempo (in alcuni casi, molte ore o giorni) per elaborare testo apparentemente innocuo.

Avvertimento

L'esempio seguente usa un'espressione regolare soggetta a un backtracking eccessivo ed è probabile che rifiuti indirizzi di posta elettronica validi. Non è consigliabile usarlo in una routine di convalida della posta elettronica. Per un'espressione regolare che convalida gli indirizzi di posta elettronica, vedere Procedura: Verificare che le stringhe siano in formato di posta elettronica valido.

Si consideri, ad esempio, un'espressione regolare comunemente usata ma problematica per convalidare l'alias di un indirizzo di posta elettronica. L'espressione ^[0-9A-Z]([-.\w]*[0-9A-Z])*$ regolare viene scritta per elaborare ciò che viene considerato un indirizzo di posta elettronica valido. Un indirizzo di posta elettronica valido è costituito da un carattere alfanumerico, seguito da zero o più caratteri che possono essere alfanumerici, punti o trattini. L'espressione regolare deve terminare con un carattere alfanumerico. Tuttavia, come illustrato nell'esempio seguente, anche se questa espressione regolare gestisce facilmente l'input valido, le prestazioni sono inefficienti quando elabora un input quasi valido:

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

Come illustrato nell'output dell'esempio precedente, il motore delle espressioni regolari elabora l'alias di posta elettronica valido in circa lo stesso intervallo di tempo indipendentemente dalla relativa lunghezza. D'altra parte, quando l'indirizzo di posta elettronica quasi valido ha più di cinque caratteri, il tempo di elaborazione circa raddoppia per ogni carattere aggiuntivo nella stringa. Pertanto, una stringa di 28 caratteri quasi valida richiederebbe più di un'ora per l'elaborazione e una stringa di 33 caratteri quasi valida richiederebbe quasi un giorno per l'elaborazione.

Poiché questa espressione regolare è stata sviluppata esclusivamente considerando il formato di input corrispondente, non tiene conto dell'input che non corrisponde al modello. Questa supervisione, a sua volta, può consentire l'input non vincolato che corrisponde quasi al modello di espressione regolare per ridurre significativamente le prestazioni.

Per risolvere questo problema, è possibile eseguire le operazioni seguenti:

  • Quando si sviluppa un modello, è consigliabile considerare come il backtracking potrebbe influire sulle prestazioni del motore delle espressioni regolari, in particolare se l'espressione regolare è progettata per elaborare l'input non vincolato. Per ulteriori informazioni, vedere la sezione Take Charge of Backtracking.

  • Verificare accuratamente la tua espressione regolare utilizzando input non validi, quasi validi e validi. È possibile usare Rex per generare in modo casuale l'input per una determinata espressione regolare. Rex è uno strumento di esplorazione di espressioni regolari di Microsoft Research.

Gestire la creazione dell'oggetto in modo appropriato

Al centro del modello a oggetti delle espressioni regolari di .NET c'è la classe System.Text.RegularExpressions.Regex, che rappresenta il motore delle espressioni regolari. Spesso, il singolo fattore più importante che influisce sulle prestazioni delle espressioni regolari è il modo in cui viene usato il Regex motore. La definizione di un'espressione regolare implica un accoppiamento stretto del motore delle espressioni regolari con un criterio di espressione regolare. Tale processo di accoppiamento è costoso, sia che si tratti di istanziare un oggetto Regex passando un pattern di espressione regolare al suo costruttore, sia che si tratti di chiamare un metodo statico passando il pattern di espressione regolare e la stringa da analizzare.

Nota

Per una descrizione dettagliata delle implicazioni sulle prestazioni dell'uso di espressioni regolari interpretate e compilate, vedere il post di blog Ottimizzazione delle prestazioni delle espressioni regolari, parte II: Esecuzione del backtracking.

È possibile associare il motore delle espressioni regolari a un criterio di espressione regolare specifico e quindi usare il motore per trovare una corrispondenza con il testo in diversi modi:

  • È possibile chiamare un metodo statico di corrispondenza dei criteri, ad esempio Regex.Match(String, String). Questo metodo non richiede la creazione di un'istanza di un oggetto espressione regolare.

  • È possibile creare un'istanza di un oggetto Regex e chiamare un metodo di pattern matching di istanza di un'espressione regolare interpretata, che è il metodo predefinito per associare il motore delle espressioni regolari a un pattern di espressione regolare. Si verifica quando viene creata un'istanza di un oggetto Regex senza un argomento options che include il flag Compiled.

  • È possibile creare un'istanza di un oggetto Regex e chiamare un metodo di pattern matching d'istanza di un'espressione regolare generata dall'origine. Questa tecnica è consigliata nella maggior parte dei casi. A tale scopo, posizionare l'attributo GeneratedRegexAttribute su un metodo parziale che restituisce Regex.

  • È possibile istanziare un oggetto Regex e chiamare un metodo di pattern matching di un'espressione regolare compilata. Gli oggetti di espressioni regolari rappresentano modelli compilati quando viene istanziato un oggetto Regex con un argomento options che include il flag Compiled.

Il modo in cui si chiamano i metodi di corrispondenza delle espressioni regolari può influire sulle prestazioni della tua applicazione. Le sezioni seguenti illustrano quando usare chiamate al metodo statico, espressioni regolari generate dall'origine, espressioni regolari interpretate e espressioni regolari compilate per migliorare le prestazioni dell'applicazione.

Importante

La forma della chiamata al metodo (statica, interpretata, generata dall'origine, compilata) influisce sulle prestazioni se la stessa espressione regolare viene usata ripetutamente nelle chiamate al metodo o se un'applicazione usa ampiamente oggetti espressione regolare.

Espressioni regolari statiche

I metodi statici di espressioni regolari sono consigliati come alternativa all'instanziazione ripetuta di un oggetto di espressione regolare con la stessa espressione. A differenza dei modelli di espressione regolare usati dagli oggetti espressione regolare, i codici operazione (opcodes) o il linguaggio intermedio comune (CIL) compilato dai modelli usati nelle chiamate ai metodi statici vengono memorizzati in cache internamente dal motore delle espressioni regolari.

Ad esempio, un gestore eventi chiama spesso un altro metodo per convalidare l'input dell'utente. Questo esempio si riflette nel codice seguente, in cui viene usato l'evento di Button un Click controllo per chiamare un metodo denominato IsValidCurrency, che controlla se l'utente ha immesso un simbolo di valuta seguito da almeno una cifra decimale.

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

Un'implementazione inefficiente del IsValidCurrency metodo è illustrata nell'esempio seguente:

Nota

Ogni chiamata al metodo reinstantia un Regex oggetto con lo stesso modello. Questo significa, a sua volta, che il modello di espressione regolare deve essere ricompilato ogni volta che il metodo viene chiamato.

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

È consigliabile sostituire il codice inefficiente precedente con una chiamata al metodo statico Regex.IsMatch(String, String) . Questo approccio elimina la necessità di creare un'istanza di un Regex oggetto ogni volta che si desidera chiamare un metodo di ricerca di criteri e consente al motore delle espressioni regolari di recuperare una versione compilata dell'espressione regolare dalla cache.

using System;
using System.Text.RegularExpressions;

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

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

Per impostazione predefinita, vengono memorizzati nella cache gli ultimi 15 modelli di espressione regolare statici usati più di recente. Per le applicazioni che richiedono un numero maggiore di espressioni regolari statiche memorizzate nella cache, le dimensioni della cache possono essere modificate impostando la Regex.CacheSize proprietà .

L'espressione \p{Sc}+\s*\d+ regolare usata in questo esempio verifica che la stringa di input abbia un simbolo di valuta e almeno una cifra decimale. Il modello è definito come illustrato nella tabella seguente:

Modello Descrizione
\p{Sc}+ Trova la corrispondenza di uno o più caratteri nella categoria Simbolo Unicode, Valuta.
\s* Trova la corrispondenza con zero o più caratteri di spazio vuoto.
\d+ Corrisponde a una o più cifre decimali.

Espressioni regolari interpretate, generate dal codice sorgente e compilate

I criteri di espressione regolare non associati al motore delle espressioni regolari tramite la specifica dell'opzione Compiled vengono interpretati. Quando viene creata un'istanza di un oggetto espressione regolare, il motore delle espressioni regolari converte l'espressione regolare in un set di codici operativi. Quando viene chiamato un metodo di istanza, i codici dell'operazione vengono convertiti in CIL ed eseguiti dal compilatore JIT. Analogamente, quando viene chiamato un metodo di espressione regolare statica e l'espressione regolare non è disponibile nella cache, il motore delle espressioni regolari converte l'espressione regolare in un set di codici operazione e li archivia nella cache. Converte quindi questi codici di operazione in CIL in modo che il compilatore JIT possa eseguirli. Le espressioni regolari interpretate riducono il tempo di avvio al costo del tempo di esecuzione più lento. A causa di questo processo, è preferibile usare quando l'espressione regolare viene usata in un numero ridotto di chiamate al metodo o se il numero esatto di chiamate ai metodi di espressione regolare è sconosciuto, ma è previsto che sia ridotto. Man mano che aumenta il numero di chiamate al metodo, il miglioramento delle prestazioni rispetto al tempo di avvio ridotto viene superato dalla velocità di esecuzione più lenta.

I criteri di espressione regolare associati al motore delle espressioni regolari tramite la specifica dell'opzione Compiled vengono compilati. Pertanto, quando viene creata un'istanza di un oggetto espressione regolare o quando viene chiamato un metodo di espressione regolare statica e non è possibile trovare l'espressione regolare nella cache, il motore delle espressioni regolari converte l'espressione regolare in un set intermedio di codici operazione. Questi codici vengono quindi convertiti in CIL. Quando viene chiamato un metodo, il compilatore JIT esegue l'CIL. A differenza delle espressioni regolari interpretate, le espressioni regolari compilate aumentano il tempo di avvio, ma eseguono i singoli metodi di corrispondenza dei criteri più velocemente. Di conseguenza, il vantaggio delle prestazioni risultante dalla compilazione dell'espressione regolare aumenta in proporzione al numero di metodi di espressione regolare chiamati.

I modelli di espressione regolare che sono associati al motore delle espressioni regolari tramite l'adornamento di un metodo che restituisce Regex con l'attributo GeneratedRegexAttribute, vengono generati dal codice sorgente. Il generatore di origine, che si collega al compilatore, genera come codice C# un'implementazione personalizzata Regexderivata con logica simile a quella RegexOptions.Compiled che genera in CIL. Si ottengono tutti i vantaggi di prestazioni di RegexOptions.Compiled (anzi, persino di più) e i vantaggi al momento dell'avvio di Regex.CompileToAssembly, ma senza la complessità di CompileToAssembly. Il codice sorgente emesso fa parte del tuo progetto, il che significa che è anche facilmente visualizzabile e di cui è possibile fare il debug.

Per riepilogare, è consigliabile:

  • Usare espressioni regolari interpretate quando si chiamano metodi per espressioni regolari con un'espressione regolare specifica relativamente di rado.
  • Usare espressioni regolari generate dall'origine se si usa Regex in C# con argomenti noti in fase di compilazione e si usa un'espressione regolare specifica relativamente frequentemente.
  • Usare espressioni regolari compilate quando si chiamano metodi per le espressioni regolari con un'espressione regolare specifica con una certa frequenza e si usa .NET 6 o una versione precedente.

È difficile determinare la soglia esatta in base alla quale le velocità di esecuzione più lente delle espressioni regolari interpretate superano i vantaggi derivanti dal tempo di avvio ridotto. È anche difficile determinare la soglia in cui i tempi di avvio più lenti delle espressioni regolari generate o compilate dall'origine superano i vantaggi derivanti dalle velocità di esecuzione più veloci. Le soglie dipendono da vari fattori, tra cui la complessità dell'espressione regolare e i dati specifici elaborati. Per determinare quali espressioni regolari offrono le migliori prestazioni per lo scenario specifico dell'applicazione, è possibile usare la classe per confrontare i Stopwatch tempi di esecuzione.

Nell'esempio seguente vengono confrontate le prestazioni delle espressioni regolari compilate, generate dall'origine e interpretate durante la lettura delle prime 10 frasi e durante la lettura di tutte le frasi nel testo di William D. Guthrie's Magna Carta e Altri indirizzi. Come illustrato nell'output dell'esempio, quando vengono effettuate solo 10 chiamate ai metodi di corrispondenza delle espressioni regolari, un'espressione regolare interpretata o generata dall'origine offre prestazioni migliori rispetto a un'espressione regolare compilata. Tuttavia, un'espressione regolare compilata offre prestazioni migliori quando viene effettuato un numero elevato di chiamate (in questo caso, oltre 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
*/

Il criterio di espressione regolare usato nell'esempio , \b(\w+((\r?\n)|,?\s))*\w+[.?:;!]è definito come illustrato nella tabella seguente:

Modello Descrizione
\b Inizia la corrispondenza al limite di una parola.
\w+ Trova la corrispondenza con uno o più caratteri di parola.
(\r?\n)|,?\s) Corrisponde a zero o a un ritorno a capo seguito da un carattere di nuova riga, oppure a zero o a una virgola seguita da un carattere di spazio bianco.
(\w+((\r?\n)|,?\s))* Trova la corrispondenza con zero o più occorrenze di uno o più caratteri di parola seguiti da zero o da un ritorno a capo e da un carattere di nuova riga oppure da zero o una virgola seguita da uno spazio vuoto.
\w+ Trova la corrispondenza con uno o più caratteri di parola.
[.?:;!] Corrisponde a un punto, punto interrogativo, due punti, punto e virgola o punto esclamativo.

Assumere la responsabilità del backtracking

In genere, il motore delle espressioni regolari usa la progressione lineare per spostarsi attraverso una stringa di input e confrontarla con un criterio di espressione regolare. Tuttavia, quando i quantificatori indeterminati come *, +, e ? vengono usati in un modello di espressione regolare, il motore delle espressioni regolari potrebbe rinunciare a una parte di corrispondenze parziali riuscite e tornare a uno stato salvato in precedenza per cercare una corrispondenza riuscita per l'intero modello. Questo processo è noto come backtracking.

Suggerimento

Per altre informazioni sul backtracking, vedere Dettagli del comportamento delle espressioni regolari e Backtracking. Per discussioni dettagliate sul backtracking, vedere i post di blog Miglioramenti delle espressioni regolari in .NET 7 e Ottimizzazione delle prestazioni delle espressioni regolari .

Le espressioni regolari acquisiscono potenza e flessibilità grazie al supporto del backtracking. Pone inoltre la responsabilità di controllare il funzionamento del motore delle espressioni regolari nelle mani degli sviluppatori di espressioni regolari. Poiché spesso gli sviluppatori non sono consapevoli di questa responsabilità, il loro uso improprio del backtracking o l'eccessiva dipendenza dal backtracking spesso svolge il ruolo più significativo nel degradare le prestazioni delle espressioni regolari. In uno scenario peggiore, il tempo di esecuzione può raddoppiare per ogni carattere aggiuntivo nella stringa di input. Infatti, usando il backtracking eccessivamente, è facile creare l'equivalente programmatico di un ciclo infinito se l'input corrisponde quasi al modello di espressione regolare. Il motore delle espressioni regolari potrebbe richiedere ore o persino giorni per elaborare una stringa di input relativamente breve.

Spesso, le applicazioni subiscono una riduzione delle prestazioni per l'uso del backtracking anche se il backtracking non è essenziale per una corrispondenza. Ad esempio, l'espressione \b\p{Lu}\w*\b regolare corrisponde a tutte le parole che iniziano con un carattere maiuscolo, come illustrato nella tabella seguente:

Modello Descrizione
\b Inizia la corrispondenza al limite di una parola.
\p{Lu} Corrisponde a un carattere maiuscolo.
\w* Trova la corrispondenza con zero o più caratteri di parola.
\b Termina la partita al confine di una parola.

Poiché un confine di parola non è uguale, né un sottoinsieme, a un carattere di parola, non è possibile che il motore delle espressioni regolari attraversi un confine di parola quando trova corrispondenze con i caratteri di parola. Pertanto, per questa espressione regolare, il backtracking non può mai contribuire al successo complessivo di qualsiasi corrispondenza. Può ridurre solo le prestazioni perché il motore delle espressioni regolari è costretto a salvare il relativo stato per ogni corrispondenza preliminare corretta di un carattere di parola.

Se si determina che il backtracking non è necessario, è possibile disabilitarlo in due modi:

  • Impostando l'opzione RegexOptions.NonBacktracking (introdotta in .NET 7). Per altre informazioni, vedere modalità Nonbacktracking.

  • Usando l'elemento (?>subexpression) del linguaggio, noto come gruppo atomico. Nell'esempio seguente viene analizzata una stringa di input usando due espressioni regolari. Il primo, \b\p{Lu}\w*\b, si basa sul backtracking. Il secondo, \b\p{Lu}(?>\w*)\b, disabilita il backtracking. Come illustrato nell'output dell'esempio, entrambi producono lo stesso risultato:

    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
    

In molti casi, il backtracking è essenziale per la corrispondenza di un modello di espressione regolare al testo di input. Tuttavia, un backtracking eccessivo può compromettere gravemente le prestazioni e creare l'impressione che un'applicazione abbia smesso di rispondere. In particolare, questo problema si verifica quando i quantificatori sono annidati e il testo che corrisponde alla sottoespressione esterna è un subset del testo che corrisponde alla sottoespressione interna.

Avvertimento

Oltre a evitare un backtracking eccessivo, è consigliabile usare la funzionalità di timeout per garantire che un backtracking eccessivo non degradi gravemente le prestazioni delle espressioni regolari. Per altre informazioni, vedere la sezione Usare i valori di timeout .

Ad esempio, il criterio ^[0-9A-Z]([-.\w]*[0-9A-Z])*\$$ di espressione regolare deve corrispondere a un numero di parte costituito da almeno un carattere alfanumerico. Qualsiasi carattere aggiuntivo può essere costituito da un carattere alfanumerico, un trattino, un carattere di sottolineatura o un punto, anche se l'ultimo carattere deve essere alfanumerico. Il numero di parte termina con un segno di dollaro. In alcuni casi, questo modello di espressione regolare può presentare prestazioni scarse perché i quantificatori sono annidati e poiché la sottoespressione [0-9A-Z] è un subset della sottoespressione [-.\w]*.

In questi casi, è possibile ottimizzare le prestazioni delle espressioni regolari rimuovendo i quantificatori annidati e sostituendo la sottoespressione esterna con un lookahead di larghezza zero o un'asserzione lookbehind. Le asserzioni Lookahead e lookbehind sono ancoraggi. Non spostano il puntatore nella stringa di input, ma guardano avanti o indietro per verificare se viene soddisfatta una condizione specificata. Ad esempio, l'espressione regolare numero di parte può essere riscritta come ^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$. Questo criterio di espressione regolare è definito come illustrato nella tabella seguente:

Modello Descrizione
^ Iniziare la corrispondenza dall'inizio della stringa di input.
[0-9A-Z] Corrispondi un carattere alfanumerico. Il numero di parte deve essere costituito almeno da questo carattere.
[-.\w]* Trova la corrispondenza con zero o più occorrenze di qualsiasi carattere di parola, trattino o punto.
\$ Abbina un segno di dollaro.
(?<=[0-9A-Z]) Guarda dietro il segno del dollaro finale per assicurarti che il carattere precedente sia alfanumerico.
$ Terminare la corrispondenza alla fine della stringa di input.

Nell'esempio seguente viene illustrato l'uso di questa espressione regolare per trovare una corrispondenza con una matrice contenente numeri di parte possibili:

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.

Il linguaggio delle espressioni regolari in .NET include gli elementi del linguaggio seguenti che è possibile usare per eliminare i quantificatori annidati. Per altre informazioni, vedere Costrutti di raggruppamento.

Elemento linguistico Descrizione
(?= subexpression ) Lookahead positivo a larghezza zero. Cerca in anticipo la posizione corrente per determinare se subexpression corrisponde alla stringa di input.
(?! subexpression ) Aspetto negativo di larghezza zero. Cerca in anticipo la posizione corrente per determinare se subexpression non corrisponde alla stringa di input.
(?<= subexpression ) Lookbehind positivo di larghezza zero. Cerca dietro la posizione corrente per determinare se subexpression corrisponde alla stringa di input.
(?<! subexpression ) Lookbehind negativo di larghezza zero. Esamina la posizione precedente per determinare se subexpression non corrisponde alla stringa di input.

Usare i valori di timeout

Se i processi delle espressioni regolari elaborano l'input che corrisponde quasi al pattern dell'espressione regolare, spesso possono fare affidamento su un backtracking eccessivo, che ne compromette significativamente le prestazioni. Oltre a considerare attentamente l'uso del backtracking e testare l'espressione regolare rispetto all'input quasi-corrispondente, è consigliabile impostare sempre un valore di timeout per ridurre al minimo l'effetto di un backtracking eccessivo, nel caso si verifichi.

L'intervallo di timeout dell'espressione regolare definisce il periodo di tempo durante il quale il motore delle espressioni regolari cercherà una singola corrispondenza prima che si verifichi un timeout. A seconda dello schema dell'espressione regolare e del testo di input, il tempo di esecuzione potrebbe superare l'intervallo di timeout specificato, ma non impiegherà più tempo per il backtracking rispetto a quanto previsto dall'intervallo di timeout. L'intervallo di timeout predefinito è Regex.InfiniteMatchTimeout, il che significa che l'espressione regolare non andrà in timeout. È possibile modificare questo valore e definire l'intervallo di timeout come segue:

Se è stato definito un intervallo di timeout e non viene trovata una corrispondenza alla fine di tale intervallo, il metodo di espressione regolare genera un'eccezione RegexMatchTimeoutException . ** Nel gestore delle eccezioni, è possibile scegliere di ritentare la corrispondenza con un intervallo di timeout più lungo, abbandonare il tentativo di corrispondenza e supporre che non esista alcuna corrispondenza, oppure registrare le informazioni sull'eccezione e abbandonare il tentativo di corrispondenza per l'analisi futura.

Nell'esempio seguente viene definito un GetWordData metodo che crea un'istanza di un'espressione regolare con un intervallo di timeout di 350 millisecondi per calcolare il numero di parole e il numero medio di caratteri in una parola in un documento di testo. Se si verifica il timeout dell'operazione corrispondente, l'intervallo di timeout viene aumentato di 350 millisecondi e l'oggetto Regex viene reintegrato. Se il nuovo intervallo di timeout supera un secondo, il metodo genera nuovamente l'eccezione al chiamante.

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

Cattura solo quando necessario

Le espressioni regolari in .NET supportano costrutti di raggruppamento, che consentono di raggruppare un criterio di espressione regolare in una o più sottoespressioni. I costrutti di raggruppamento usati più comunemente nel linguaggio delle espressioni regolari .NET sono (sottoespressione, che definisce un gruppo di acquisizione numerato e ) una(?<>del nome), che definisce un gruppo di acquisizione denominato. I costrutti di raggruppamento sono essenziali per la creazione di backreference e per la definizione di una sottoespressione a cui viene applicato un quantificatore.

Tuttavia, l'uso di questi elementi del linguaggio ha un costo. Determinano il riempimento dell'oggetto GroupCollection restituito dalla proprietà Match.Groups con le acquisizioni denominate o senza nome più recenti. Se un singolo costrutto di raggruppamento ha acquisito più sottostringhe nella stringa di input, popola anche l'oggetto CaptureCollection restituito dalla Group.Captures proprietà di un determinato gruppo di acquisizione con più Capture oggetti.

Spesso, i costrutti di raggruppamento vengono usati in un'espressione regolare solo in modo che i quantificatori possano essere applicati. I gruppi acquisiti da queste sottoespressioni non vengono usati in un secondo momento. Ad esempio, l'espressione \b(\w+[;,]?\s?)+[.?!] regolare è progettata per acquisire un'intera frase. Nella tabella seguente vengono descritti gli elementi del linguaggio in questo modello di espressione regolare e il loro effetto sulle raccolte Match e Match.Groups dell'oggetto Group.Captures.

Modello Descrizione
\b Inizia la corrispondenza al limite di una parola.
\w+ Trova la corrispondenza con uno o più caratteri di parola.
[;,]? Corrisponde a zero o una virgola o punto e virgola.
\s? Trova la corrispondenza con zero o uno spazio vuoto.
(\w+[;,]?\s?)+ Trova una o più occorrenze di uno o più caratteri di parola seguiti da una virgola facoltativa o da un punto e virgola facoltativo, seguiti da un carattere di spazio facoltativo. Questo modello definisce il primo gruppo di acquisizione, necessario in modo che la combinazione di più caratteri di parola (ovvero una parola) seguita da un simbolo di punteggiatura facoltativa venga ripetuta fino a quando il motore delle espressioni regolari non raggiunge la fine di una frase.
[.?!] Corrisponde a un punto, un punto interrogativo o un punto esclamativo.

Come illustrato nell'esempio seguente, quando viene trovata una corrispondenza, gli oggetti GroupCollection e CaptureCollection vengono entrambi popolati con le acquisizioni della corrispondenza. In questo caso, il gruppo di cattura (\w+[;,]?\s?) esiste affinché il quantificatore + possa essere applicato su di esso, il che consente al modello di espressione regolare di corrispondere a ogni parola in una frase. In caso contrario, corrisponderebbe all'ultima parola in 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.

Quando si usano sottoespressioni solo per applicare quantificatori e non si è interessati al testo acquisito, è consigliabile disabilitare le acquisizioni di gruppo. Ad esempio, l'elemento (?:subexpression) del linguaggio impedisce al gruppo a cui si applica di acquisire sottostringhe corrispondenti. Nell'esempio seguente il criterio di espressione regolare dell'esempio precedente viene modificato in \b(?:\w+[;,]?\s?)+[.?!]. Come illustrato nell'output, impedisce al motore delle espressioni regolari di popolare le raccolte GroupCollection e CaptureCollection.

using System;
using System.Text.RegularExpressions;

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

        foreach (Match match in Regex.Matches(input, pattern))
        {
            Console.WriteLine($"Match: '{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.

È possibile disabilitare le acquisizioni in uno dei modi seguenti:

  • Usare l'elemento (?:subexpression) del linguaggio. Questo elemento impedisce l'acquisizione di sottostringhe corrispondenti nel gruppo a cui si applica. Non disabilita le acquisizioni di sottostringa in alcun gruppo annidato.

  • Usare l'opzione ExplicitCapture. Disabilita tutte le acquisizioni non denominate o implicite nel modello di espressione regolare. Quando si usa questa opzione, è possibile acquisire solo sottostringhe che corrispondono a gruppi denominati definiti con l'elemento (?<name>subexpression) del linguaggio. Il ExplicitCapture flag può essere passato al options parametro di un Regex costruttore di classe o al options parametro di un Regex metodo di corrispondenza statico.

  • Usare l'opzione n nell'elemento (?imnsx) del linguaggio. Questa opzione disabilita tutte le acquisizioni non denominate o implicite dal punto nel modello di espressione regolare in cui appare l'elemento. Le catture vengono disabilitate fino alla fine del modello o fino a quando l'opzione (-n) abilita le catture non denominate o implicite. Per altre informazioni, vedere Costrutti vari.

  • Usare l'opzione n nell'elemento (?imnsx:subexpression) del linguaggio. Questa opzione disabilita tutte le acquisizioni non denominate o implicite in subexpression. Anche le acquisizioni da parte di gruppi di acquisizione annidati senza nome o impliciti sono disabilitate.

Sicurezza dei thread

La Regex classe stessa è thread-safe e non modificabile (sola lettura). Vale a dire, Regex gli oggetti possono essere creati in qualsiasi thread e condivisi tra thread. I metodi di corrispondenza possono essere chiamati da qualsiasi thread e non modificare mai uno stato globale.

Tuttavia, gli oggetti risultato (Match e MatchCollection) restituiti da Regex devono essere usati in un singolo thread. Sebbene molti di questi oggetti siano logicamente non modificabili, le loro implementazioni potrebbero ritardare il calcolo di alcuni risultati per migliorare le prestazioni e, di conseguenza, i chiamanti devono serializzare l'accesso.

Se è necessario condividere Regex oggetti risultato su più thread, questi oggetti possono essere convertiti in istanze thread-safe chiamando i relativi metodi sincronizzati. Ad eccezione degli enumeratori, tutte le classi di espressioni regolari sono thread-safe o possono essere convertite in oggetti thread-safe da un metodo sincronizzato.

Gli enumeratori sono l'unica eccezione. È necessario serializzare le chiamate agli enumeratori di raccolta. La regola è che se una raccolta può essere enumerata contemporaneamente in più thread, è necessario sincronizzare i metodi dell'enumeratore sull'oggetto radice della raccolta attraversata dall'enumeratore.

Titolo Descrizione
Dettagli del comportamento delle espressioni regolari Esamina l'implementazione del motore delle espressioni regolari in .NET. L'articolo è incentrato sulla flessibilità delle espressioni regolari e illustra la responsabilità dello sviluppatore di garantire il funzionamento efficiente e affidabile del motore delle espressioni regolari.
Ritorno sui propri passi Viene illustrato il backtracking e il modo in cui influisce sulle prestazioni delle espressioni regolari ed esamina gli elementi del linguaggio che forniscono alternative al backtracking.
Linguaggio di espressioni regolari - Riferimento rapido Vengono descritti gli elementi del linguaggio delle espressioni regolari in .NET e vengono forniti collegamenti a documentazione dettagliata per ogni elemento del linguaggio.