Condividi tramite


Procedure consigliate per le espressioni regolari in .NET

Il motore delle espressioni regolari in .NET è uno strumento potente e completo che consente di elaborare il testo in base alle corrispondenze dei modelli anziché in base al confronto e alla corrispondenza con il testo letterale. Nella maggior parte dei casi, la corrispondenza dei modelli viene applicata in modo rapido ed efficiente. In alcuni casi, tuttavia, il motore delle espressioni regolari può risultare molto lento. In casi estremi, può anche sembrare che il motore non risponda durante l'elaborazione di un input relativamente piccolo per ore o perfino giorni.

In questo articolo vengono illustrate alcune procedure consigliate che possono essere adottate dagli sviluppatori per ottenere prestazioni ottimali con le espressioni regolari.

Avviso

Quando si usa System.Text.RegularExpressions per elaborare l'input non attendibile, passare un timeout. Un utente malintenzionato può fornire input a RegularExpressions, provocando un attacco Denial of Service. Le API del framework ASP.NET Core che usano RegularExpressions passano un timeout.

Esaminare l'origine di input

In generale, le espressioni regolari possono accettare due tipi di input: vincolato o non vincolato. Per input vincolato si intende un testo che proviene da un'origine conosciuta o affidabile e segue un formato predefinito. Per input non vincolato si intende un testo che proviene da un'origine non affidabile, ad esempio un utente Web, e potrebbe non seguire un formato predefinito o previsto.

I criteri di espressione regolare vengono spesso scritti in modo da corrispondere all'input valido. ovvero gli sviluppatori esaminano il testo per il quale desiderano trovare una corrispondenza e scrivono quindi un modello di espressione regolare a esso corrispondente. Gli sviluppatori determinano infine se questo modello richiede una correzione o un'ulteriore elaborazione testandolo con più elementi di input validi. Se il criterio corrisponde a tutti gli input considerati validi, viene dichiarato pronto per la produzione e può essere incluso in un'applicazione rilasciata. Con questo approccio un criterio di espressione regolare viene considerato appropriato per la corrispondenza con un input vincolato. Tuttavia, non verrà considerato appropriato per la corrispondenza con un input non vincolato.

Per corrispondere a un input non vincolato, un'espressione regolare deve gestire in modo efficiente tre tipi di testo:

  • Testo che corrisponde al modello di espressione regolare.
  • Testo che non corrisponde al criterio di espressione regolare.
  • Testo che corrisponde quasi al modello 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 sul backtracking esteso, il motore delle espressioni regolari può richiedere una quantità eccessiva di tempo, in alcuni casi molte ore o giorni, per l'elaborazione di un testo apparentemente irrilevante.

Avviso

Nell'esempio seguente viene usata un'espressione regolare soggetta a un backtracking eccessivo e che con tutta probabilità rifiuta indirizzi di posta elettronica validi. Non usarla in una routine di convalida di 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 la convalida dell'alias di un indirizzo di posta elettronica. L'espressione regolare ^[0-9A-Z]([-.\w]*[0-9A-Z])*$ 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, sebbene questa espressione regolare gestisca facilmente l'input valido, le prestazioni risulteranno inefficienti quando viene elaborato 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 nello stesso intervallo di tempo indipendentemente dalla lunghezza. D'altra parte, quando l'indirizzo di posta elettronica quasi valido ha più di cinque caratteri, il tempo di elaborazione raddoppia approssimativamente per ogni carattere aggiuntivo nella stringa. L'elaborazione di una stringa di 28 caratteri quasi valida richiederebbe pertanto più di un'ora e l'elaborazione di una stringa di 33 caratteri quasi valida richiederebbe quasi un giorno.

Poiché questa espressione regolare è stata sviluppata considerando unicamente la corrispondenza con il formato di input, l'input che non corrisponde al criterio non viene preso in considerazione. Questa svista può consentire a un input non vincolato che corrisponde quasi al criterio di espressione regolare di ridurre significativamente le prestazioni.

Per risolvere tale problema, è possibile effettuare le operazioni seguenti:

  • Durante lo sviluppo di un modello, è consigliabile considerare il modo in cui il backtracking potrebbe influire sulle prestazioni del motore delle espressioni regolari, soprattutto se l'espressione regolare è progettata per elaborare un input non vincolato. Per altre informazioni, vedere la sezione Assumere il controllo del backtracking.

  • Testare accuratamente l'espressione regolare usando input non valido, quasi valido e valido. È 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 di istanze degli oggetti in modo appropriato

Il modello a oggetti delle espressioni regolari di .NET è basato sulla classe System.Text.RegularExpressions.Regex, che rappresenta il motore delle espressioni regolari. Il fattore principale che spesso influisce sulle prestazioni delle espressioni regolari è il modo in cui viene utilizzato il motore Regex. Per definire un'espressione regolare è necessario associare il motore delle espressioni regolari a un modello di espressione regolare. Tale processo di associazione è dispendioso, indipendentemente dal fatto che comporti la creazione di un'istanza di un oggetto Regex passando al relativo costruttore un criterio di espressione regolare o la chiamata a un metodo statico passando il criterio di espressione regolare e la stringa da analizzare.

Nota

Per informazioni dettagliate sull'impatto che l'uso delle espressioni regolari interpretate e compilate può avere sulle prestazioni, vedere il post del blogOttimizzazione delle prestazioni delle espressioni regolari - Parte II: Controllo 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 modelli, ad esempio Regex.Match(String, String). Questo metodo non richiede la creazione di un'istanza di un oggetto di espressione regolare.

  • È possibile creare un'istanza di un oggetto Regex e chiamare un metodo di corrispondenza dei criteri di istanza di un'espressione regolare interpretata, ovvero il metodo predefinito per associare il motore delle espressioni regolari a un criterio di espressione regolare. Viene utilizzato quando l'istanza di un oggetto Regex viene creata senza un argomento options che include il flag Compiled.

  • È possibile creare un oggetto Regex e chiamare un metodo di corrispondenza dei modelli dell'istanza di un'espressione 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 creare un'istanza di un oggetto Regex e chiamare un metodo di corrispondenza dei modelli dell'istanza di un'espressione regolare compilata. Gli oggetti di espressioni regolari rappresentano i modelli compilati quando l'istanza di un oggetto Regex viene creata con un argomento options che include il flag Compiled.

Il modo specifico in cui si chiamano metodi di corrispondenza delle espressioni regolari può influire sulle prestazioni dell'applicazione. Nelle sezioni seguenti viene illustrato quando utilizzare le chiamate al metodo statico, le espressioni regolari generate dall'origine, le espressioni regolari interpretate e le espressioni regolari compilate per migliorare le prestazioni dell'applicazione.

Importante

Il formato della chiamata al metodo (statico, interpretato, generato dall'origine, compilato) influisce sulle prestazioni se la stessa espressione regolare viene utilizzata più volte nelle chiamate al metodo oppure se in un'applicazione vengono utilizzati spesso gli oggetti di espressione regolare.

Espressioni regolari statiche

I metodi con espressioni regolari statiche sono consigliati come alternativa alla creazione ripetuta di un'istanza di un oggetto di espressione regolare con la stessa espressione regolare. A differenza dei criteri di espressione regolare usati dagli oggetti di espressione regolare, i codici operativi (opcode) o il Common Intermediate Language (CIL) compilato dei criteri usati nelle chiamate al metodo statico vengono memorizzati nella cache interna dal motore delle espressioni regolari.

Ad esempio, un gestore eventi chiama frequentemente un altro metodo per convalidare l'input dell'utente. Questo esempio viene rispecchiato nel codice seguente, in cui l'evento Click di un controllo Button viene usato 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

Nell'esempio seguente viene illustrata un'implementazione inefficiente del metodo IsValidCurrency:

Nota

Ogni chiamata al metodo crea una nuova istanza dell'oggetto Regex con lo stesso criterio. Di conseguenza, il modello di espressione regolare deve essere ricompilato ogni volta che viene chiamato il metodo.

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 evita di dover creare un'istanza di un oggetto Regex ogni volta che si vuole chiamare un metodo di corrispondenza dei 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, nella cache vengono memorizzati gli ultimi 15 modelli di espressione regolare statica usati più di recente. Per le applicazioni che richiedono un numero maggiore di espressioni regolari statiche memorizzate nella cache, la dimensione della cache può essere modificata impostando la proprietà Regex.CacheSize.

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

Modello Descrizione
\p{Sc}+ Trova uno o più caratteri nella categoria Unicode Symbol, Currency.
\s* Trova zero o più spazi vuoti.
\d+ Trova una o più cifre decimali.

Espressioni regolari interpretate o generate da origini o compilate

I criteri di espressione regolare che non sono associati al motore delle espressioni mediante l'opzione Compiled vengono interpretati. Quando viene creata un'istanza di un oggetto di 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 operativi vengono convertiti in CIL ed eseguiti dal compilatore JIT. Analogamente, quando viene chiamato un metodo con espressioni regolari statiche e l'espressione regolare non è presente nella cache, il motore delle espressioni regolari converte l'espressione regolare in un set di codici operativi che memorizza nella cache. Converte quindi i codici operativi in CIL in modo tale che possano essere eseguiti dal compilatore JIT. Le espressioni regolari interpretate consentono di ridurre il tempo di avvio ma implicano tempi di esecuzione più lenti. A causa di questo processo, risultano particolarmente adatte quando l'espressione regolare viene usata in un numero limitato di chiamate al metodo o se il numero esatto di chiamate ai metodi delle espressioni regolari è sconosciuto ma si prevede che sia esiguo. Man mano che aumenta il numero di chiamate al metodo, il miglioramento delle prestazioni rispetto alla riduzione del tempo di avvio viene superato dalla minore velocità di esecuzione.

I modelli di espressione regolare che sono associati al motore delle espressioni regolari mediante l'opzione Compiled vengono compilati. Quando viene creata un'istanza di un oggetto di espressione regolare o quando viene chiamato un metodo con espressioni regolari statiche e l'espressione regolare non è presente nella cache, il motore delle espressioni regolari converte pertanto l'espressione regolare in un set intermedio di codici operativi, il quale viene quindi convertito in MSIL. Questi codici vengono quindi convertiti in CIL. Quando viene chiamato un metodo, il compilatore JIT esegue il CIL. A differenza delle espressioni regolari interpretate, le espressioni regolari compilate aumentano il tempo di avvio eseguendo i singoli metodi di corrispondenza dei modelli più velocemente. Di conseguenza, il vantaggio in termini di prestazioni che risulta dalla compilazione dell'espressione regolare aumenta proporzionalmente al numero di metodi dell'espressione regolare chiamati.

I criteri di espressione regolare associati al motore delle espressioni regolari tramite l'adornamento di un metodo Regex-returning con l'attributo GeneratedRegexAttribute sono generati dall’origine. Il generatore di origini, che collega il compilatore, genera come codice C# un'implementazione personalizzata Regexderivata con logica simile a quella che RegexOptions.Compiled genera in CIL. Si ottengono tutti i vantaggi in termini di prestazioni della velocità effettiva di RegexOptions.Compiled e di avvio di Regex.CompileToAssembly, anzi di più, ma senza la complessità di CompileToAssembly. L'origine generata fa parte del progetto, il che significa che è anche facilmente visualizzabile e sottoponibile a debug.

Per riepilogare, si consiglia di effettuare le operazioni seguenti:

  • Usare espressioni regolari interpretate quando i metodi dell'espressione regolare vengono chiamati frequentemente con un'espressione regolare specifica non molto spesso.
  • Usare espressioni regolari generate all'origine se si usa Regex in C# con argomenti noti in fase di compilazione e si usa un'espressione regolare specifica con una certa frequenza.
  • Usare espressioni regolari compilate quando i metodi dell'espressione regolare vengono chiamati frequentemente con un'espressione regolare specifica e si usa .NET 6 o una versione precedente.

È difficile determinare la soglia esatta in cui la minore velocità di esecuzione delle espressioni regolari interpretate supera i vantaggi derivanti dalla riduzione del tempo di avvio. È anche difficile determinare la soglia in cui i tempi di avvio più lenti delle espressioni regolari generate o compilate all'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 che vengono elaborati. Per determinare quali espressioni regolari interpretate o compilate offrono le migliori prestazioni per lo scenario specifico dell'applicazione, è possibile utilizzare la classe Stopwatch per confrontare i rispettivi 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 Magna Carta, and Other Addresses di William D. Guthrie. Come illustrato nell'output dell'esempio, quando vengono effettuate solo 10 chiamate ai metodi di corrispondenza delle espressioni regolari, un'espressione regolare o generata dall’origine interpretata offre prestazioni migliori rispetto a un'espressione regolare compilata. Tuttavia, un'espressione regolare compilata offre prestazioni migliori quando viene effettuato un numero di chiamate maggiore, 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("   {0} matches in {1}", ctr, sw.Elapsed);

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

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

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

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

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

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

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

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

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

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

Assumere il controllo del backtracking

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

Suggerimento

Per altre informazioni sul backtracking, vedere Dettagli sul 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.

Il supporto del backtracking fornisce alle espressioni regolari potenza e flessibilità. Inoltre la responsabilità del controllo del funzionamento del motore delle espressioni regolari viene affidata agli sviluppatori delle espressioni regolari. Poiché spesso gli sviluppatori non sono consapevoli di questa responsabilità, un utilizzo improprio del backtracking o un utilizzo eccessivo del backtracking rappresenta spesso la causa principale della riduzione delle prestazioni delle espressioni regolari. Nello scenario peggiore, il tempo di esecuzione può raddoppiarsi per ogni carattere aggiuntivo nella stringa di input. Un uso eccessivo del backtracking porta infatti facilmente a creare l'equivalente programmatico di un ciclo infinito se l'input corrisponde quasi al criterio 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 regolare \b\p{Lu}\w*\b cerca una corrispondenza di tutte le parole che iniziano con un carattere maiuscolo, come illustrato nella tabella seguente:

Modello Descrizione
\b Inizia la corrispondenza sul confine di parola.
\p{Lu} Trova un carattere maiuscolo.
\w* Trova zero o più caratteri alfanumerici.
\b Termina la corrispondenza sul confine di parola.

Poiché un confine di parola non è uguale a un carattere alfanumerico né è un subset di tali caratteri, non è possibile che il motore delle espressioni regolari attraversi un confine di parola quando viene trovata una corrispondenza con i caratteri alfanumerici. Pertanto, per questa espressione regolare, il backtracking non può mai contribuire al successo complessivo di qualsiasi corrispondenza. Può solo ridurre 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à non backtracking.

  • Usando l'elemento del linguaggio (?>subexpression), noto come gruppo atomico. Nell'esempio seguente viene analizzata una stringa di input utilizzando due espressioni regolari. La prima, \b\p{Lu}\w*\b, si basa sul backtracking. La seconda, \b\p{Lu}(?>\w*)\b, disabilita il backtracking. Come illustrato nell'output dell'esempio, entrambe 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 con il testo di input. Tuttavia, un utilizzo eccessivo del backtracking può ridurre notevolmente le prestazioni e dare l'impressione che un'applicazione non risponda. In particolare, questo problema si verifica quando vengono annidati i quantificatori e il testo che corrisponde alla sottoespressione esterna è un subset del testo che corrisponde alla sottoespressione interna.

Avviso

Oltre a evitare un eccessivo utilizzo del backtracking, è necessario usare la funzionalità di timeout per assicurarsi che l'eccessivo backtracking non comprometta troppo le prestazioni dell'espressione regolare. Per altre informazioni, vedere la sezione Usare valori di timeout.

Ad esempio, il criterio di espressione regolare ^[0-9A-Z]([-.\w]*[0-9A-Z])*\$$ viene usato per trovare la corrispondenza con un numero parte costituito da almeno un carattere alfanumerico. Tutti i caratteri aggiuntivi possono essere costituiti da un carattere alfanumerico, un trattino, un carattere di sottolineatura o un punto, sebbene l'ultimo carattere debba essere alfanumerico. Il numero parte termina con il simbolo del dollaro. In alcuni casi, il criterio di espressione regolare può presentare prestazioni insufficienti perché vengono annidati i quantificatori e perché la sottoespressione [0-9A-Z] è un subset della sottoespressione [-.\w]*.

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

Modello Descrizione
^ Inizia la corrispondenza all'inizio della stringa di input.
[0-9A-Z] Trova la corrispondenza di un carattere alfanumerico. Il numero parte deve essere costituito da almeno uno di questi caratteri.
[-.\w]* Trova la corrispondenza di zero o più occorrenze di un carattere alfanumerico, un trattino o un punto.
\$ Trova la corrispondenza di un simbolo del dollaro.
(?<=[0-9A-Z]) Esegue il lookbehind rispetto al simbolo del dollaro finale per verificare che il carattere precedente sia alfanumerico.
$ Termina la ricerca della corrispondenza alla fine della stringa di input.

Nell'esempio seguente viene illustrato l'utilizzo dell'espressione regolare per trovare la corrispondenza con una matrice contenente i numeri 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 i seguenti elementi che è possibile usare per eliminare i quantificatori annidati. Per altre informazioni, vedere Costrutti di raggruppamento.

Elemento di linguaggio Descrizione
(?= subexpression ) Asserzione lookahead positiva di larghezza zero. Esegue il lookahead rispetto alla posizione corrente per determinare se subexpression corrisponde alla stringa di input.
(?! subexpression ) Asserzione lookahead negativa di larghezza zero. Esegue il lookahead rispetto alla posizione corrente per determinare se subexpression non corrisponde alla stringa di input.
(?<= subexpression ) Lookbehind positivo di larghezza zero. Esegue il lookbehind rispetto alla posizione corrente per determinare se subexpression corrisponde alla stringa di input.
(?<! subexpression ) Lookbehind negativo di larghezza zero. Esegue il lookbehind rispetto alla posizione corrente per determinare se subexpression non corrisponde alla stringa di input.

Usare valori di timeout

Se le espressioni regolari elaborano l'input che corrisponde quasi al modello dell'espressione regolare, possono spesso basarsi su un backtracking eccessivo, con un impatto notevole sulle prestazioni. Oltre a considerare attentamente l'utilizzo del backtracking e a testare l'espressione regolare rispetto all'input quasi corrispondente, è necessario impostare sempre un valore di timeout per ridurre al minimo l'impatto di un eventuale backtracking eccessivo.

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 del timeout. A seconda del criterio di espressione regolare e del testo di input, il tempo di esecuzione potrebbe superare l'intervallo di timeout specificato, ma il tempo dedicato al backtracking non sarà superiore rispetto all'intervallo di timeout specificato. L'intervallo di timeout predefinito è Regex.InfiniteMatchTimeout, che indica nessun timeout per l'espressione regolare. È possibile eseguire l'override di questo valore e definire un intervallo di timeout come segue:

Se è stato definito un intervallo di timeout e non viene trovata alcuna corrispondenza alla fine di questo intervallo, il metodo dell'espressione regolare genera un'eccezione RegexMatchTimeoutException. Nel gestore eccezioni è possibile continuare a cercare la corrispondenza con un intervallo di timeout più lungo, abbandonare il tentativo di ricerca supponendo che non esista alcuna corrispondenza oppure abbandonare il tentativo di ricerca e registrare le informazioni sull'eccezione per un'analisi futura.

Nell'esempio seguente viene definito un metodo GetWordData 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 di una parola in un documento di testo. Se l'operazione di ricerca della corrispondenza scade, l'intervallo di timeout viene aumentato di 350 millisecondi e viene nuovamente creata un'istanza dell'oggetto Regex. 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:               {0:N0}", info.Item1);
            Console.WriteLine("Average Word Length: {0:N2} characters", info.Item2);
        }
        catch (IOException e)
        {
            Console.WriteLine("IOException reading file '{0}'", title);
            Console.WriteLine(e.Message);
        }
        catch (RegexMatchTimeoutException e)
        {
            Console.WriteLine("The operation timed out after {0:N0} milliseconds",
                              e.MatchTimeout.TotalMilliseconds);
        }
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

Eseguire l'acquisizione solo quando necessario

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

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

I costrutti di raggruppamento vengono spesso usati in un'espressione regolare solo in modo da consentire l'applicazione dei quantificatori. I gruppi acquisiti da queste sottoespressioni non vengono usati in un secondo momento. Ad esempio, l'espressione regolare \b(\w+[;,]?\s?)+[.?!] è progettata per acquisire un'intera frase. Nella tabella seguente vengono descritti gli elementi del linguaggio di tale criterio di espressione regolare e il relativo effetto sulle raccolte Match e Match.Groups dell'oggetto Group.Captures:

Modello Descrizione
\b Inizia la corrispondenza sul confine di parola.
\w+ Trova uno o più caratteri alfanumerici.
[;,]? Trova zero o una virgola oppure zero o un punto e virgola.
\s? Trova zero o uno spazio vuoto.
(\w+[;,]?\s?)+ Trova una o più occorrenze di uno o più caratteri alfanumerici seguiti da una virgola o un punto e virgola facoltativo seguito da uno spazio vuoto facoltativo. Questo criterio definisce il primo gruppo di acquisizione, necessario affinché la combinazione di più caratteri alfanumerici, ovvero una parola, seguiti da un simbolo di punteggiatura facoltativo venga ripetuta finché il motore delle espressioni regolari non raggiunge la fine di una frase.
[.?!] Trova un punto, un punto interrogativo o un punto esclamativo.

Come illustrato nell'esempio seguente, quando viene trovata una corrispondenza, entrambi gli oggetti GroupCollection e CaptureCollection vengono popolati con le acquisizioni della corrispondenza. In questo caso, è presente il gruppo di acquisizione (\w+[;,]?\s?) in modo tale che sia possibile applicarvi il quantificatore + consentendo al criterio di espressione regolare di trovare la corrispondenza con ogni parola di una frase. In caso contrario, verrebbe trovata la corrispondenza con l'ultima parola di 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: '{0}' at index {1}.",
                              match.Value, match.Index);
            int grpCtr = 0;
            foreach (Group grp in match.Groups)
            {
                Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                                  grpCtr, grp.Value, grp.Index);
                int capCtr = 0;
                foreach (Capture cap in grp.Captures)
                {
                    Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                      capCtr, cap.Value, cap.Index);
                    capCtr++;
                }
                grpCtr++;
            }
            Console.WriteLine();
        }
    }
}
// The example displays the following output:
//       Match: 'This is one sentence.' at index 0.
//          Group 0: 'This is one sentence.' at index 0.
//             Capture 0: 'This is one sentence.' at 0.
//          Group 1: 'sentence' at index 12.
//             Capture 0: 'This ' at 0.
//             Capture 1: 'is ' at 5.
//             Capture 2: 'one ' at 8.
//             Capture 3: 'sentence' at 12.
//
//       Match: 'This is another.' at index 22.
//          Group 0: 'This is another.' at index 22.
//             Capture 0: 'This is another.' at 22.
//          Group 1: 'another' at index 30.
//             Capture 0: 'This ' at 22.
//             Capture 1: 'is ' at 27.
//             Capture 2: 'another' at 30.
Imports System.Text.RegularExpressions

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

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

Quando le sottoespressioni vengono usate solo per applicarvi i quantificatori e non è necessario il testo acquisito, è consigliabile disabilitare le acquisizioni del gruppo. Ad esempio, l'elemento del linguaggio (?:subexpression) impedisce al gruppo al quale viene applicato di acquisire le sottostringhe corrispondenti. Nell'esempio seguente il criterio di espressione regolare dell'esempio precedente viene modificato in \b(?:\w+[;,]?\s?)+[.?!]. Come illustrato nell'output, il motore delle espressioni regolari non popolerà 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: '{0}' at index {1}.",
                              match.Value, match.Index);
            int grpCtr = 0;
            foreach (Group grp in match.Groups)
            {
                Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                                  grpCtr, grp.Value, grp.Index);
                int capCtr = 0;
                foreach (Capture cap in grp.Captures)
                {
                    Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                      capCtr, cap.Value, cap.Index);
                    capCtr++;
                }
                grpCtr++;
            }
            Console.WriteLine();
        }
    }
}
// The example displays the following output:
//       Match: 'This is one sentence.' at index 0.
//          Group 0: 'This is one sentence.' at index 0.
//             Capture 0: 'This is one sentence.' at 0.
//
//       Match: 'This is another.' at index 22.
//          Group 0: 'This is another.' at index 22.
//             Capture 0: 'This is another.' at 22.
Imports System.Text.RegularExpressions

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

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

È possibile disabilitare le acquisizioni in uno dei modi seguenti:

  • Usare l'elemento del linguaggio (?:subexpression). Questo elemento impedisce l'acquisizione delle sottostringhe corrispondenti nel gruppo a cui viene applicato. Non disabilita le acquisizioni delle sottostringhe in tutti i gruppi annidati.

  • 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 le sottostringhe che corrispondono ai gruppi denominati definiti con l'elemento del linguaggio (?<name>subexpression). Il flag ExplicitCapture può essere passato al parametro options del costruttore della classe Regex o al parametro options di un metodo Regex statico corrispondente.

  • Utilizzare l'opzione n nell'elemento del linguaggio (?imnsx). Questa opzione disabilita tutte le acquisizioni non denominate o implicite dal punto nel modello di espressione regolare in corrispondenza del quale viene visualizzato l'elemento. Le acquisizioni vengono disabilitate fino alla fine del modello o finché l'opzione (-n) non abilita le acquisizioni non denominate o implicite. Per altre informazioni, vedere Costrutti vari.

  • Utilizzare l'opzione n nell'elemento del linguaggio (?imnsx:subexpression). Questa opzione disabilita tutte le acquisizioni non denominate o implicite in subexpression. Vengono inoltre disabilitate tutte le acquisizioni dai gruppi di acquisizione annidati non denominati o impliciti.

Thread safety

La classe Regex è thread-safe e non modificabile (di sola lettura). Ciò significa che è possibile creare oggetti Regex in qualsiasi thread e condividerli fra i thread; i metodi corrispondenti possono essere chiamati da qualsiasi thread e non modificano in nessun caso 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 non siano modificabili a livello logico, le loro implementazioni potrebbero ritardare l'elaborazione di determinati risultati per migliorare le prestazioni; di conseguenza, i chiamanti dovranno serializzare l'accesso a tali oggetti.

Se risulta necessario condividere oggetti risultato Regex su più thread, tali oggetti possono essere convertiti in istanze thread-safe chiamando i relativi metodi sincronizzati. Fatta eccezione per gli enumeratori, tutte le classi di espressioni regolari sono thread-safe o possono essere convertite in oggetti thread-safe mediante un metodo sincronizzato.

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

Posizione Descrizione
Dettagli sul comportamento delle espressioni regolari Viene esaminata l'implementazione del motore delle espressioni regolari in .NET. L'articolo è incentrato sulla flessibilità delle espressioni regolari e sulla responsabilità dello sviluppatore al fine di garantire un funzionamento efficace e affidabile del motore delle espressioni regolari.
Backtracking Viene illustrato il backtracking e il modo in cui influisce sulle prestazioni delle espressioni regolari e vengono esaminati gli elementi del linguaggio che forniscono le alternative al backtracking.
Linguaggio di espressioni regolari - Riferimento rapido Vengono illustrati gli elementi del linguaggio delle espressioni regolari in .NET e vengono forniti i collegamenti alla documentazione dettagliata per ogni elemento del linguaggio.