Partager via


Bonnes pratiques pour les expressions régulières dans .NET

Le moteur d’expression régulière dans .NET est un outil puissant et complet. Il traite le texte en fonction de correspondances de modèle plutôt qu’en comparant et en faisant correspondre le texte littéral. Dans la plupart des cas, il exécute les critères spéciaux de façon rapide et efficace. Toutefois, dans certains cas, le moteur des expressions régulières peut sembler lent. Dans des cas extrêmes, il semble même cesser de répondre. Il traite en effet peu d'entrées sur une période de plusieurs heures ou même de plusieurs jours.

Cet article décrit quelques-unes des meilleures pratiques que les développeurs peuvent adopter afin de garantir que les expressions régulières atteignent des performances optimales.

Avertissement

Lorsque vous utilisez System.Text.RegularExpressions pour traiter une entrée non approuvée, passez un délai d’expiration. Un utilisateur malveillant peut fournir une entrée à RegularExpressions, provoquant une attaque par déni de service. Les API d’infrastructure ASP.NET Core qui utilisent RegularExpressions passent un délai d’expiration.

Prise en compte de la source d’entrée

En général, les expressions régulières peuvent accepter deux types d'entrée : avec contrainte ou sans contrainte. L’entrée avec contrainte est un texte provenant d’une source fiable ou connue, et qui suit un format prédéfini. L’entrée sans contrainte est un texte provenant d’une source non fiable, telle qu’un utilisateur web. Elle ne suit pas forcément un format prédéfini ou attendu.

Les modèles d’expressions régulières sont souvent écrits pour correspondre à une entrée valide. Autrement dit, les développeurs examinent le texte qu'ils souhaitent faire correspondre, puis ils écrivent un modèle d'expression régulière qui lui correspond. Les développeurs déterminent ensuite si ce modèle doit être corrigé ou approfondi en le testant à l'aide de plusieurs éléments d'entrée valides. Lorsque le modèle correspond à toutes les entrées valides supposées, il est déclaré prêt pour la production et peut être intégré à une application finale. Avec cette approche, le modèle d’expression régulière est adapté à la mise en correspondance d’une entrée avec contrainte. Toutefois, il n’est pas adapté à la mise en correspondance d’une entrée sans contrainte.

Pour faire correspondre une entrée sans contrainte, une expression régulière doit pouvoir gérer efficacement trois types de texte :

  • Texte correspondant au modèle d’expression régulière.
  • Texte ne correspondant pas au modèle d’expression régulière.
  • Texte correspondant presque au modèle d’expression régulière.

Le dernier type de texte est problématique pour une expression régulière écrite pour gérer les entrées avec contrainte. Si cette expression régulière repose également sur une rétroaction complète, le traitement d’un texte apparemment anodin par le moteur d’expression régulière risque d’être extrêmement long (dans certains cas, un grand nombre d’heures ou de jours).

Avertissement

L’exemple suivant utilise une expression régulière sujette à des rétroactions excessives et susceptible de rejeter des adresses e-mail valides. Vous ne devez pas l’utiliser dans une routine de validation d’e-mails. Si vous souhaitez une expression régulière qui valide des adresses e-mail, consultez Guide pratique : vérifier que des chaînes sont dans un format d’adresse e-mail valide.

Prenons l’exemple d’une expression régulière fréquemment utilisée, mais problématique, pour la validation de l’alias d’une adresse e-mail. L’expression régulière ^[0-9A-Z]([-.\w]*[0-9A-Z])*$ est écrite pour traiter ce qui est considéré comme une adresse e-mail valide. Une adresse e-mail valide se compose d’un caractère alphanumérique suivi de zéro, ou de plusieurs caractères (caractères alphanumériques, points ou traits d’union). L'expression régulière doit se terminer par un caractère alphanumérique. Toutefois, comme illustré dans l’exemple suivant, bien que cette expression régulière gère facilement une entrée valide, elle s’avère inefficace lorsqu’il s’agit de traiter une entrée presque valide :

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

Comme le montre la sortie de l’exemple précédent, le moteur des expressions régulières traite l’alias de messagerie valide dans un intervalle de temps à peu près identique, indépendamment de sa longueur. En revanche, lorsque l’adresse e-mail presque valide comporte plus de cinq caractères, le temps de traitement est environ doublé pour chaque caractère supplémentaire de la chaîne. Ainsi, une chaîne de 28 caractères presque valide serait traitée en plus d’une heure et qu’une chaîne de 33 caractères presque valide serait traitée en un peu moins d’un jour.

Étant donné que cette expression régulière a été développée en prenant uniquement en considération le format de l’entrée à faire correspondre, elle ne tient pas compte des entrées qui ne correspondent pas au modèle. Une entrée sans contrainte correspondant presque au modèle d’expression régulière risque ainsi de nuire considérablement aux performances.

Pour résoudre ce problème, vous pouvez effectuer les opérations suivantes :

  • Lorsque vous développez un modèle, vous devez réfléchir à la manière dont la rétroaction peut affecter les performances du moteur des expressions régulières, en particulier si votre expression régulière est conçue pour traiter des entrées sans contrainte. Pour plus d’informations, consultez la section Prise en charge de la rétroaction.

  • Testez soigneusement votre expression régulière à l’aide d’une entrée non valide, quasi valide et valide. Vous pouvez utiliser Rex pour générer de manière aléatoire une entrée pour une expression régulière particulière. Rex est un outil d’exploration d’expressions régulières de Microsoft Research.

Gestion correcte de l’instanciation d’objet

La classe System.Text.RegularExpressions.Regex est au cœur du modèle objet d'expression régulière de .NET. Elle représente le moteur d’expressions régulières. Souvent, la façon dont le moteur Regex est utilisé est le facteur principal ayant un impact sur les performances des expressions régulières. La définition d’une expression régulière implique une association étroite entre le moteur des expressions régulières et un modèle d’expression régulière. Ce processus est onéreux, qu’il implique l’instanciation d’un objet Regex en passant à son constructeur un modèle d’expression régulière ou en appelant une méthode statique en lui passant le modèle d’expression régulière avec la chaîne à analyser.

Remarque

Vous trouverez une présentation plus détaillée des répercussions sur les performances des expressions régulières interprétées dans le billet de blog Optimiser les performances des expressions régulières, deuxième partie : prendre en charge le retour sur trace.

Vous pouvez associer le moteur des expressions régulières à un modèle d’expression régulière spécifique, puis utiliser le moteur pour faire correspondre du texte de plusieurs façons :

  • Vous pouvez appeler une méthode statique de critères spéciaux, comme Regex.Match(String, String). L’instanciation d’un objet d’expression régulière n’est pas nécessaire pour cette méthode.

  • Vous pouvez instancier un objet Regex et appeler une méthode de correspondance de modèle d’instance d’une expression régulière interprétée, qui est la méthode par défaut pour lier le moteur d’expression régulière à un modèle d’expression régulière. Elle se produit lorsqu'un objet Regex est instancié sans argument options incluant l'indicateur Compiled.

  • Vous pouvez instancier un objet Regex et appeler une méthode de correspondance d’instance d’une expression régulière générée par le code source. Cette technique est recommandée dans la plupart des cas. Pour ce faire, placez l’attribut GeneratedRegexAttribute sur une méthode partielle qui retourne Regex.

  • Vous pouvez instancier un objet Regex et appeler une méthode d'instance de critères spéciaux d'une expression régulière compilée. Les objets d’expression régulière représentent des modèles compilés lorsqu’un objet Regex est instancié avec un argument options incluant l’indicateur Compiled.

La façon particulière dont vous appelez des méthodes de correspondance d’expression régulière peut affecter les performances de votre application. Les sections suivantes expliquent quand utiliser les appels de méthode statique, les expressions régulières générées par le code source, les expressions régulières interprétées et les expressions régulières compilées afin d’améliorer les performances de votre application.

Important

La forme de l’appel de méthode (statique, interprétée, générée par le code source, compilée) affecte les performances si une même expression régulière est utilisée à plusieurs reprises dans les appels de méthode, ou si une application entraîne l’utilisation intensive d’objets d’expression régulière.

Expressions régulières statiques

Les méthodes d'expression régulière statiques sont recommandées pour éviter d'instancier à plusieurs reprises un objet d'expression régulière avec la même expression régulière. À la différence des modèles d’expressions régulières utilisés par les objets d’expression régulière, les codes d’opération (opcodes) ou le code CIL (Common Intermediate Language) compilé des modèles utilisés dans les appels de méthode statique sont mis en cache en interne par le moteur d’expression régulière.

Par exemple, un gestionnaire d'événements appelle fréquemment une autre méthode pour valider l'entrée d'utilisateur. Cet exemple se reflète dans le code suivant, dans lequel l’événement Button d’un contrôle Click est utilisé pour appeler une méthode nommée IsValidCurrency, qui vérifie si l’utilisateur a entré un symbole monétaire suivi d’au moins un chiffre décimal.

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

L’exemple suivant illustre une implémentation peu efficace de la méthode IsValidCurrency :

Notes

Chaque appel de méthode réinstancie un objet Regex avec le même modèle. Cela signifie ainsi que le modèle d’expression régulière doit être recompilé chaque fois que la méthode est appelée.

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

Vous devez remplacer le code précédent, peu efficace, par un appel à la méthode statique Regex.IsMatch(String, String). Avec cette approche, un objet Regex n’a pas besoin d’être instancié chaque fois que vous souhaitez appeler une méthode de critères spéciaux. En outre, le moteur des expressions régulières est alors en mesure de récupérer une version compilée de l’expression régulière depuis son 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

Par défaut, les 15 derniers modèles d’expressions régulières statiques utilisés récemment sont mis en cache. Pour les applications qui requièrent un plus grand nombre d'expressions régulières statiques mises en cache, la taille du cache peut être ajustée en définissant la propriété Regex.CacheSize.

L’expression régulière \p{Sc}+\s*\d+ utilisée dans cet exemple vérifie que la chaîne d’entrée se compose d’un symbole monétaire et d’au moins un chiffre décimal. Le modèle est défini comme indiqué dans le tableau suivant :

Modèle Description
\p{Sc}+ Met en correspondance un ou plusieurs caractères dans la catégorie Unicode symbole, devise.
\s* Correspond à zéro, un ou plusieurs espaces blancs.
\d+ Met en correspondance un ou plusieurs chiffres décimaux.

Expressions régulières interprétées, générées par le code source ou compilées

Les modèles d’expressions régulières qui ne sont pas associés au moteur d’expression régulière par la spécification de l’option Compiled sont interprétés. Lorsqu'un objet d'expression régulière est instancié, le moteur des expressions régulières convertit l'expression régulière en un ensemble de codes d'opération. Lorsqu’une méthode d’instance est appelée, les codes d’opération sont convertis en code CIL et exécutés par le compilateur JIT. De même, lorsqu’une méthode d’expression régulière statique est appelée et que l’expression régulière ne peut pas être récupérée dans le cache, le moteur des expressions régulières convertit l’expression régulière en un ensemble de codes d’opération et les stocke dans le cache. Il convertit ensuite ces codes d’opération en code CIL afin que le compilateur JIT puisse les exécuter. Les expressions régulières interprétées réduisent le temps de démarrage, mais ralentissent le temps d'exécution. En raison de ce processus, il est préférable de les utiliser lorsque l’expression régulière est utilisée dans un nombre d’appels de méthode restreint, ou lorsque le nombre exact d’appels de méthodes d’expression régulière est inconnu, mais qu’il est supposé être petit. À mesure que le nombre d'appels de méthode augmente, le ralentissement de la vitesse d'exécution l'emporte sur l'amélioration des performances liée à la réduction du temps de démarrage.

Les modèles d’expressions régulières qui sont associés au moteur d’expression régulière par la spécification de l’option Compiled sont compilés. Ainsi, lorsqu’un objet d’expression régulière est instancié ou lorsqu’une méthode d’expression régulière statique est appelée et que l’expression régulière ne peut pas être récupérée dans le cache, le moteur des expressions régulières convertit l’expression régulière en un ensemble de codes d’opération intermédiaire. Ces codes sont ensuite convertis en CIL. Lorsqu’une méthode est appelée, le compilateur JIT exécute le code CIL. Contrairement aux expressions régulières interprétées, les expressions régulières compilées augmentent le temps de démarrage, mais elles exécutent plus rapidement les méthodes de critères spéciaux individuelles. En conséquence, l'amélioration des performances due à la compilation de l'expression régulière augmente en fonction du nombre de méthodes d'expression régulières appelées.

Les modèles d’expressions régulières qui sont associés au moteur d’expression régulière par l’ornement d’une méthode retournant Regex avec l’attribut GeneratedRegexAttribute sont générés par le code source. Le générateur de code source, qui se connecte au compilateur, émet sous forme de code C# une implémentation dérivée de Regex personnalisée avec une logique similaire à ce que RegexOptions.Compiled émet en langage intermédiaire. Vous bénéficiez de tous les avantages (voire plus) en matière de performances de débit de RegexOptions.Compiled et des avantages de démarrage de Regex.CompileToAssembly, mais sans la complexité de CompileToAssembly. La source émise fait partie de votre projet, ce qui signifie qu’elle est facilement visible et débogable.

En résumé, voici nos recommandations :

  • Utilisez des expressions régulières interprétées lorsque vous appelez relativement peu des méthodes d’expression régulière avec une expression régulière spécifique.
  • Utilisez des expressions régulières générées par le code source si vous vous servez de Regex en C# avec des arguments connus au moment de la compilation et d’une expression régulière spécifique relativement fréquemment.
  • Utilisez des expressions régulières compilées lorsque vous appelez relativement souvent des méthodes d’expression régulière avec une expression régulière spécifique et que vous vous servez de .NET 6 ou d’une version antérieure.

Il est difficile de déterminer le seuil exact auquel le temps gagné au démarrage ne compense plus les vitesses d’exécution plus lentes des expressions régulières interprétées. Il est également difficile de déterminer le seuil auquel les vitesses d’exécution plus rapides ne compensent plus les temps de démarrage plus lents des expressions régulières générées par le code source ou compilées. Les seuils dépendent de divers facteurs, notamment la complexité des expressions régulières et les données spécifiques qu’elles traitent. Pour déterminer quelles expressions régulières offrent les meilleures performances pour votre scénario d’application spécifique, vous pouvez utiliser la classe Stopwatch pour comparer les temps d’exécution.

L’exemple suivant compare les performances des expressions régulières compilées, générées par le code source et interprétées sur la lecture des 10 premières phrases et de toutes les phrases du texte de Theodore Dreiser, The Financier. Comme l’indique la sortie de l’exemple, lorsque les méthodes de correspondance d’expression régulière sont appelées seulement 10 fois, une expression régulière interprétée ou générée par le code source offre de meilleures performances qu’une expression régulière compilée. Par contre, une expression régulière compilée offre de meilleures performances dans le cas d'un grand nombre d'appels (dans le cas présent, plus de 13 000).

const string pattern = @"\b(\w+((\r?\n)|,?\s))*\w+[.?:;!]";

[GeneratedRegex(pattern, RegexOptions.Singleline)]
private static partial Regex GeneratedRegex();

public static void RunIt()
{
    Stopwatch sw;
    Match match;
    int ctr;

    StreamReader inFile = new(@".\Dreiser_TheFinancier.txt");
    string input = inFile.ReadToEnd();
    inFile.Close();

    // 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(input);
    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(input);
    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(input);
    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(input);
    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(input);
    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(input);
    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);
}
/* The example displays the following output:

   10 Sentences with Interpreted Regex:
       10 matches in 00:00:00.0050027
   10 Sentences with Compiled Regex:
       10 matches in 00:00:00.0181372
   10 Sentences with Source-generated Regex:
       10 matches in 00:00:00.0049145
   All Sentences with Interpreted Regex:
       13,682 matches in 00:00:00.1588303
   All Sentences with Compiled Regex:
       13,682 matches in 00:00:00.0859949
   All Sentences with Source-generated Regex:
       13,682 matches in 00:00:00.2794411
*/

Le modèle d’expression régulière utilisé dans l’exemple, \b(\w+((\r?\n)|,?\s))*\w+[.?:;!], est défini comme indiqué dans le tableau suivant :

Modèle Description
\b Commencer la correspondance à la limite d'un mot.
\w+ Met en correspondance un ou plusieurs caractères alphabétiques.
(\r?\n)|,?\s) Met en correspondance un ou aucun retour chariot suivi d’un caractère de saut de ligne, ou une ou aucune virgule, suivie d’un espace blanc.
(\w+((\r?\n)|,?\s))* Met en correspondance zéro, une ou plusieurs occurrences d’un ou plusieurs caractères alphabétiques qui sont suivis par un ou aucun retour chariot et un caractère de saut de ligne, ou par une ou aucune virgule, suivie d’un espace blanc.
\w+ Met en correspondance un ou plusieurs caractères alphabétiques.
[.?:;!] Met en correspondance un point, un point d’interrogation, deux-points, un point-virgule ou un point d’exclamation.

Prise en charge de la rétroaction

Normalement, le moteur des expressions régulières utilise une progression linéaire pour se déplacer dans une chaîne d’entrée et pour la comparer à un modèle d’expression régulière. Toutefois, lorsque les quantificateurs indéterminés, *, + et ?, par exemple, sont utilisés dans un modèle d’expression régulière, le moteur des expressions régulières peut abandonner une partie des correspondances partielles trouvées et revenir à un état précédemment enregistré pour trouver une correspondance pour le modèle entier. Ce processus est appelé « rétroaction ».

Conseil

Pour plus d'informations sur le retour arrière, consultez les pages Informations sur le comportement des expressions régulières et Retour arrière. Pour obtenir des discussions détaillées sur le retour arrière, consultez les améliorations apportées aux expressions régulières dans .NET 7 et l’optimisation des billets de blog sur les performances des expressions régulières.

La prise en charge de la rétroaction confère aux expressions régulières leur puissance et leur flexibilité. La responsabilité de contrôle du fonctionnement du moteur des expressions régulières est alors confiée aux développeurs d'expressions régulières. Souvent, les développeurs ne sont pas conscients de cette responsabilité. Leur utilisation incorrecte de la rétroaction ou leur dépendance vis-à-vis d'une rétroaction excessive a souvent un impact négatif très important sur les performances des expressions régulières. Dans le pire des scénarios, la durée d'exécution peut doubler pour chaque caractère supplémentaire de la chaîne d'entrée. En réalité, lorsque la rétroaction est utilisée de manière excessive, il est facile de créer l’équivalent de programmation d’une boucle sans fin si l’entrée correspond presque au modèle d’expression régulière. Le moteur des expressions régulières peut alors traiter une chaîne d’entrée relativement courte en plusieurs heures, voire en plusieurs jours.

Souvent, les applications subissent des pertes de performances pour l’utilisation d’un retour sur trace, alors qu’il n’est pas essentiel pour une correspondance. Par exemple, l’expression régulière \b\p{Lu}\w*\b établit une correspondance entre tous les mots qui commencent par une majuscule, comme indiqué dans le tableau suivant :

Modèle Description
\b Commencer la correspondance à la limite d'un mot.
\p{Lu} Met en correspondance une majuscule.
\w* Met en correspondance zéro, un ou plusieurs caractères alphabétiques.
\b Terminer la correspondance à la limite d'un mot.

Étant donné qu’une limite de mot est différente d’un caractère alphabétique et qu’elle n’est pas un sous-ensemble de ce dernier, il est impossible que le moteur des expressions régulières franchisse une limite de mot lors de la mise en correspondance de caractères alphabétiques. Cela signifie que, pour cette expression régulière, une rétroaction ne peut jamais contribuer à la réussite globale d’une correspondance. Cela risque en revanche de diminuer les performances, étant donné que le moteur des expressions régulières doit impérativement enregistrer sont état pour chaque correspondance préliminaire d’un caractère alphabétique trouvée.

Si vous concluez que le retour arrière n'est pas nécessaire, vous pouvez le désactiver de deux façons :

  • En définissant l’option RegexOptions.NonBacktracking (introduite dans .NET 7). Pour plus d’informations, consultez Mode de non retour sur trace.

  • En utilisant l’élément de langage (?>subexpression), appelé groupe atomique. L'exemple suivant analyse une chaîne d'entrée à l'aide de deux expressions régulières. La première, \b\p{Lu}\w*\b, utilise la rétroaction. La seconde, \b\p{Lu}(?>\w*)\b, désactive la rétroaction. Comme l’indique la sortie de l’exemple, les résultats obtenus sont identiques :

    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
    

Dans de nombreux cas, la rétroaction est essentielle pour faire correspondre un modèle d’expression régulière à un texte d’entrée. Toutefois, une rétroaction excessive risque d'altérer considérablement les performances et de donner l'impression qu'une application a cessé de répondre. Ce problème se produit notamment lorsque les quantificateurs sont imbriqués et que le texte correspondant à la sous-expression externe est un sous-ensemble du texte correspondant à la sous-expression interne.

Avertissement

En plus d’éviter une rétroaction excessive, vous devez utiliser la fonctionnalité de délai d’attente pour garantir qu’une rétroaction excessive n’altère pas considérablement les performances des expressions régulières. Pour plus d’informations, consultez la section Utilisation de valeurs de délai d’attente.

Par exemple, le modèle d'expression régulière ^[0-9A-Z]([-.\w]*[0-9A-Z])*\$$ est conçu pour mettre en correspondance un numéro de référence composé d'au moins un caractère alphanumérique. Tous les caractères supplémentaires peuvent être composés d'un caractère alphanumérique, d'un trait d'union, d'un trait de soulignement ou d'un point. Toutefois, le dernier caractère doit impérativement être alphanumérique. Un signe dollar termine le numéro de référence. Dans certains cas, ce modèle d’expression régulière peut présenter des performances médiocres, si les quantificateurs sont imbriqués et que la sous-expression [0-9A-Z] est un sous-ensemble de la sous-expression [-.\w]*.

Dans ces cas, vous pouvez optimiser les performances des expressions régulières en supprimant les quantificateurs imbriqués et en remplaçant la sous-expression externe par une assertion de préanalyse ou de postanalyse de largeur nulle. Les assertions avant et arrière sont des ancres. Elles ne déplacent pas le pointeur dans la chaîne d’entrée, mais elles vérifient en amont et en aval si une condition spécifiée est remplie. Par exemple, l'expression régulière de numéro de référence peut être réécrite sous la forme ^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$. Ce modèle d’expression régulière est défini comme indiqué dans le tableau suivant :

Modèle Description
^ Commencer la correspondance au début de la chaîne d'entrée.
[0-9A-Z] Mettre en correspondance un caractère alphanumérique. Le numéro de référence doit au minimum comporter ce caractère.
[-.\w]* Mettre en correspondance zéro, une ou plusieurs occurrences de tout caractère alphabétique, trait d'union ou point.
\$ Mettre en correspondance un signe dollar.
(?<=[0-9A-Z]) Effectuer une préanalyse avancée du signe dollar de fin de s’assurer que le caractère précédent est un caractère alphanumérique.
$ Terminer la correspondance à la fin de la chaîne d'entrée.

L’exemple suivant illustre l’utilisation de cette expression régulière pour faire correspondre un tableau pouvant contenir des numéros de référence potentiels :

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.

Le langage d’expression régulière dans .NET comprend les éléments de langage suivants, que vous pouvez utiliser pour éliminer les quantificateurs imbriqués. Pour plus d’informations, consultez Constructions de regroupement.

Élément du langage Description
(?= subexpression ) Préanalyse positive de largeur nulle. Effectue une préanalyse de la position actuelle pour déterminer si subexpression correspond à la chaîne d’entrée.
(?! subexpression ) Préanalyse négative de largeur nulle. Effectue une préanalyse de la position actuelle pour déterminer si subexpression ne correspond pas à la chaîne d’entrée.
(?<= subexpression ) Postanalyse positive de largeur nulle. Effectue une postanalyse de la position actuelle pour déterminer si subexpression correspond à la chaîne d’entrée.
(?<! subexpression ) Postanalyse négative de largeur nulle. Effectue une postanalyse de la position actuelle pour déterminer si subexpression ne correspond pas à la chaîne d’entrée.

Utilisation de valeurs de délai d’attente

Si une expression régulière traite une entrée qui correspond presque au modèle d'expression régulière, elle peut souvent se baser sur une rétroaction excessive, laquelle affecte considérablement ses performances. En plus d’envisager soigneusement l’utilisation de la rétroaction et de tester l’expression régulière sur une entrée presque correspondante, vous devez toujours définir une valeur de délai d’attente pour garantir la réduction de l’impact d’une rétroaction excessive, le cas échéant.

L’intervalle de délai d’expiration de l’expression régulière définit la période pendant laquelle le moteur d’expression régulière recherche une correspondance unique avant son expiration. Selon le modèle d’expression régulière et le texte d’entrée, le temps d’exécution peut dépasser l’intervalle de délai d’expiration spécifié, mais il ne passe pas plus de temps de retour en arrière que l’intervalle de délai d’attente spécifié. L’intervalle de délai d’expiration par défaut est Regex.InfiniteMatchTimeout, ce qui signifie que l’expression régulière n’expire pas. Vous pouvez remplacer cette valeur et définir un intervalle de délai d’attente comme suit :

Si vous avez défini un délai d’attente et qu’aucune correspondance n’est trouvée à la fin de cet intervalle, la méthode d’expression régulière lève une exception RegexMatchTimeoutException. Dans votre gestionnaire d’exceptions, vous pouvez choisir de réessayer la mise en correspondance avec un délai d’attente plus long, d’annuler la recherche de correspondance et de supposer l’absence de correspondance, ou encore d’annuler la recherche de correspondance et de consigner les informations sur les exceptions à des fins d’analyse ultérieure.

L'exemple ci-dessous définit une méthode GetWordData qui instancie une expression régulière avec un délai d'attente de 350 millisecondes pour calculer le nombre de mots dans un document texte et le nombre moyen de caractères par mot. Si le délai d’attente de l’opération de recherche de correspondance expire, le délai d’attente augmente de 350 millisecondes et l’objet Regex est réinstancié. Si le nouveau délai d’attente dépasse une seconde, la méthode lève de nouveau l’exception pour l’appelant.

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

Capture uniquement quand cela s’avère nécessaire

Les expressions régulières dans .NET prennent les constructions de regroupement, ce qui vous permet de regrouper un modèle d’expression régulière dans une ou plusieurs sous-expressions. Les constructions de regroupement les plus fréquemment utilisées dans le langage d’expression régulière .NET sont (sous-expression), qui définit un groupe de capture numéroté et (?<nom>sous-expression), qui définit un groupe de capture nommé. Les constructions de regroupement sont essentielles pour créer des références arrières et pour définir une sous-expression à laquelle un quantificateur est appliqué.

Toutefois, l'utilisation de ces éléments de langage n'est pas sans effet. Elles entraînent le remplissage de l’objet GroupCollection retourné par la propriété Match.Groups avec les captures non nommées ou nommées les plus récentes. Si une seule construction de regroupement a capturé plusieurs sous-chaînes dans la chaîne d’entrée, elles remplissent également l’objet CaptureCollection renvoyé par la propriété Group.Captures d’un groupe de capture particulier à l’aide de plusieurs objets Capture.

Souvent, les constructions de regroupement sont utilisées dans une expression régulière uniquement pour que les quantificateurs puissent leur être appliqués. Les groupes capturés par ces sous-expressions ne sont alors pas utilisés par la suite. Par exemple, l'expression régulière \b(\w+[;,]?\s?)+[.?!] est conçue pour capturer une phrase entière. Le tableau suivant décrit les éléments de langage dans ce modèle d’expression régulière et leur effet sur les Match des objets Match.Groups et les collections Group.Captures :

Modèle Description
\b Commencer la correspondance à la limite d'un mot.
\w+ Met en correspondance un ou plusieurs caractères alphabétiques.
[;,]? Met en correspondance zéro ou une virgule, ou zéro ou un point-virgule.
\s? Met en correspondance zéro ou un espace blanc.
(\w+[;,]?\s?)+ Met en correspondance une ou plusieurs occurrences d’un ou plusieurs caractères alphabétiques suivis d’une virgule ou d’un point-virgule facultatif suivi d’un espace blanc facultatif. Ce modèle définit le premier groupe de capture. Il est nécessaire pour que la combinaison de plusieurs caractères alphabétiques (autrement dit, un mot) suivis d’un signe de ponctuation facultatif soit répétée jusqu’à ce que le moteur des expressions régulières ait atteint la fin d’une phrase.
[.?!] Met en correspondance un point, un point d’interrogation ou un point d’exclamation.

Comme l'indique l'exemple suivant, lorsqu'une correspondance est trouvée, l'objet GroupCollection et l'objet CaptureCollection sont remplis avec des captures de la correspondance. Dans ce cas, le groupe de capture (\w+[;,]?\s?) existe afin que le quantificateur + puisse lui être appliqué, ce qui permet au modèle d'expression régulière de correspondre à chaque mot d'une phrase. Sinon, elle correspondrait au dernier mot d'une phrase.

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.

Lorsque vous utilisez des sous-expressions uniquement pour y appliquer des quantificateurs et que le texte capturé ne vous intéresse pas, vous devez désactiver les captures de groupe. Par exemple, l'élément de langage (?:subexpression) empêche le groupe auquel il s'applique de capturer les sous-chaînes correspondantes. Dans l'exemple suivant, le modèle d'expression régulière de l'exemple précédent est remplacé par \b(?:\w+[;,]?\s?)+[.?!]. Comme l’indique la sortie, le moteur des expressions régulières ne peut pas remplir les collections GroupCollection et 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.

Vous pouvez désactiver les captures de l'une des façons suivantes :

  • Utilisez l'élément de langage (?:subexpression). Cet élément empêche la capture des sous-chaînes correspondantes dans le groupe auquel il s'applique. Il ne désactive pas les captures de la sous-chaîne dans les groupes imbriqués.

  • Utilisez l'option ExplicitCapture. Elle désactive toutes les captures implicites ou sans nom dans le modèle d’expression régulière. Avec cette option, seules les sous-chaînes qui correspondent à des groupes nommés définis avec l'élément de langage (?<name>subexpression) peuvent être capturées. L'indicateur ExplicitCapture peut être passé au paramètre options d'un constructeur de classe Regex ou au paramètre options d'une méthode correspondante statique Regex.

  • Utilisez l'option n dans l'élément de langage (?imnsx). Cette option désactive toutes les captures implicites ou sans nom à partir du point où l’élément apparaît dans le modèle d’expression régulière. Les captures sont désactivées jusqu’à la fin du modèle ou jusqu’à ce que l’option (-n) active les captures implicites ou sans nom. Pour plus d'informations, consultez Miscellaneous Constructs.

  • Utilisez l'option n dans l'élément de langage (?imnsx:subexpression). Cette option désactive toutes les captures implicites ou sans nom dans subexpression. Les captures effectuées par les groupes de capture imbriqués implicites ou sans nom sont également désactivées.

Sécurité des threads

La classe Regex proprement dite est thread-safe et immuable (en lecture seule). Autrement dit, des objets Regex peuvent être créés sur n’importe quel thread et partagés par plusieurs threads ; les méthodes de mise en correspondance peuvent être appelées à partir de n’importe quel thread et ne modifient jamais un état global.

Toutefois, les objets de résultat (Match et MatchCollection) retournés par Regex doivent être utilisés sur un seul thread. Bien que bon nombre de ces objets soient logiquement immuables, leurs implémentations peuvent retarder le calcul de certains résultats pour améliorer les performances, et par conséquent, les appelants doivent en sérialiser l’accès.

Si vous avez besoin de partager des objets de résultat Regex sur plusieurs threads, ces objets peuvent être convertis en instances thread-safe en appelant leurs méthodes synchronisées. À l’exception des énumérateurs, toutes les classes d’expression régulière sont thread-safe ou peuvent être converties en objets thread-safe par une méthode synchronisée.

Les énumérateurs sont la seule exception. Vous devez sérialiser les appels aux énumérateurs de collections. La règle est que si une collection peut être énumérée simultanément sur plusieurs threads, vous devez synchroniser les méthodes d’énumération sur l’objet racine de la collection traversée par l’énumérateur.

Intitulé Description
Comportement détaillé des expressions régulières Aborde l’implémentation du moteur d’expression régulière dans .NET. Cet article traite de la flexibilité des expressions régulières. Elle explique la responsabilité du développeur pour que le fonctionnement efficace et fiable du moteur des expressions régulières soit garanti.
Rétroaction Aborde la rétroaction et la façon dont elle affecte les performances des expressions régulières, ainsi que les éléments de langage, qui offrent des alternatives à la rétroaction.
Langage des expressions régulières - Aide-mémoire Décrit les éléments du langage d’expression régulière dans .NET et propose des liens vers la documentation détaillée pour chaque élément de langage.