Empfohlene Vorgehensweisen für die Verwendung von regulären Ausdrücken in .NET

Die Engine für reguläre Ausdrücke in .NET ist ein leistungsstarkes Tool mit vollem Funktionsumfang, das Texte auf Grundlage von Musterübereinstimmungen verarbeitet, anstatt Literaltext zu vergleichen und nach Übereinstimmungen mit diesem zu suchen. In den meisten Fällen wird die Suche nach Musterabgleichen schnell und effizient ausgeführt. Gelegentlich kann die Engine für reguläre Ausdrücke jedoch langsam wirken. In Extremfällen kann auch der Eindruck entstehen, dass das Modul nicht mehr reagiert, wenn für die Verarbeitung relativ kleiner Eingaben mehrere Stunden oder sogar Tage benötigt werden.

In diesem Artikel werden einige bewährte Methoden erläutert, mit denen Entwickler*innen sicherstellen können, dass ihre regulären Ausdrücke die optimale Leistung erzielen.

Warnung

Übergeben Sie ein Timeout, wenn Sie System.Text.RegularExpressions zum Verarbeiten nicht vertrauenswürdiger Eingaben verwenden. Böswillige Benutzer können Eingaben für RegularExpressions bereitstellen, um einen Denial-of-Service-Angriff durchzuführen. ASP.NET Core-Framework-APIs, die RegularExpressions verwenden, übergeben ein Timeout.

Bedenken der Eingabequelle

Im Allgemeinen können reguläre Ausdrücke zwei Arten von Eingaben annehmen: eingeschränkte und nicht eingeschränkte. Bei eingeschränkten Eingaben handelt es sich um einen Text, der aus einer bekannten oder zuverlässigen Quelle stammt und einem vordefinierten Format entspricht. Eine nicht eingeschränkte Eingabe ist ein Text, der aus einer unzuverlässigen Quelle stammt (z. B. von Webbenutzer*innen) und keinem vordefinierten oder erwarteten Format entspricht.

Muster regulärer Ausdrücke werden oft für den Abgleich mit gültigen Eingaben geschrieben. Das bedeutet, dass ein Entwickler den Text überprüft, mit dem eine Übereinstimmung erzielt werden soll, und anschließend ein entsprechendes Muster für reguläre Ausdrücke erstellt. Dann ermittelt der Entwickler, ob dieses Muster Korrekturen oder Ausarbeitungen erfordert, indem er es mit mehreren gültigen Eingabeelementen testet. Wenn das Muster mit allen als gültig geltenden Eingaben übereinstimmt, wird es als produktionsbereit deklariert und kann in eine veröffentlichte Anwendung eingeschlossen werden. Durch diesen Ansatz eignet sich das Muster des regulären Ausdrucks für den Abgleich mit eingeschränkten Eingaben. Es eignet sich jedoch nicht für den Abgleich mit nicht eingeschränkten Eingaben.

Zum Abgleichen nicht eingeschränkter Eingaben muss ein regulärer Ausdruck drei Arten von Text effizient verarbeiten:

  • Text, der mit dem Muster eines regulären Ausdrucks übereinstimmt.
  • Text, der nicht mit dem Muster des regulären Ausdrucks übereinstimmt
  • Text, der fast mit dem Muster eines regulären Ausdrucks übereinstimmt.

Der letzte Texttyp ist besonders problematisch für reguläre Ausdrücke, die für die Behandlung eingeschränkter Eingaben vorgesehen sind. Wenn ein solcher regulärer Ausdruck zudem auf umfangreicher Rückverfolgung beruht, kann die Engine für reguläre Ausdrücke für die Verarbeitung von scheinbar harmlosem Text übermäßig lange Zeit brauchen (in manchen Fällen mehrere Stunden oder Tage).

Warnung

Im folgenden Beispiel wird ein regulärer Ausdruck verwendet, der für übermäßige Rückverfolgung anfällig ist und wahrscheinlich gültige E-Mail-Adressen zurückweist. Sie sollten ihn nicht in einer E-Mail-Validierungsroutine verwenden. Einen regulären Ausdruck, der E-Mail-Adressen überprüft, finden Sie unter Vorgehensweise: Überprüfen, ob Zeichenfolgen ein gültiges E-Mail-Format aufweisen.

Als Beispiel dient hier ein häufig verwendeter, jedoch problematischer regulärer Ausdruck zum Überprüfen des Alias einer E-Mail-Adresse. Der reguläre Ausdruck ^[0-9A-Z]([-.\w]*[0-9A-Z])*$ dient zum Verarbeiten einer als gültig geltenden E-Mail-Adresse. Eine gültige E-Mail-Adresse besteht aus einem alphanumerischen Zeichen gefolgt von keinem oder mehreren Zeichen, bei denen es sich um alphanumerische Zeichen, Punkte oder Bindestriche handeln kann. Der reguläre Ausdruck muss mit einem alphanumerischen Zeichen enden. Wie das folgende Beispiel zeigt, kann dieser reguläre Ausdruck gültige Eingaben problemlos verarbeiten, bei der Verarbeitung von fast gültigen Eingaben ist seine Leistung jedoch schlecht:

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

Die Ausgabe im obigen Beispiel zeigt, dass die Engine für reguläre Ausdrücke den gültigen E-Mail-Alias in etwa demselben Zeitintervall verarbeitet, unabhängig von dessen Länge. Wenn die fast gültige E-Mail-Adresse mehr als fünf Zeichen aufweist, verdoppelt sich die Verarbeitungszeit allerdings nahezu für jedes weitere Zeichen in der Zeichenfolge. Die Verarbeitung einer fast gültigen Zeichenfolge mit 28 Zeichen würde daher mehr als eine Stunde und die einer fast gültigen Zeichenfolge mit 33 Zeichen ungefähr einen Tag dauern.

Da dieser reguläre Ausdruck ausschließlich im Hinblick auf das Format der abzugleichenden Eingaben entwickelt wurde, werden Eingaben, die nicht mit dem Muster übereinstimmen, nicht berücksichtigt. Diese „Versehen“ können wiederum dazu führen, dass die Leistung durch nicht eingeschränkte Eingaben, die fast mit dem Muster des regulären Ausdrucks übereinstimmen, erheblich beeinträchtigt wird.

Um dieses Problem zu beheben, können Sie wie folgt vorgehen:

  • Beim Erstellen eines Musters sollten Sie berücksichtigen, wie sich das Zurückverfolgen auf die Leistung der Engine für reguläre Ausdrücke auswirken könnte. Dies gilt insbesondere, wenn ein regulärer Ausdruck für die Verarbeitung nicht eingeschränkter Eingaben vorgesehen ist. Weitere Informationen finden Sie im Abschnitt Steuern der Rückverfolgung.

  • Testen Sie den regulären Ausdruck gründlich mit ungültigen, fast gültigen und gültigen Eingaben. Sie können Rex verwenden, um nach dem Zufallsprinzip Eingaben für einen bestimmten regulären Ausdruck zu generieren. Rex ist ein Tool zum Untersuchen von regulären Ausdrücken von Microsoft Research.

Angemessene Behandlung der Objektinstanziierung

Den Kern des .NET-Objektmodells für reguläre Ausdrücke bildet die System.Text.RegularExpressions.Regex-Klasse, die die Engine für reguläre Ausdrücke darstellt. Häufig ist die einzige Hauptursache für Leistungsbeeinträchtigungen bei regulären Ausdrücken die Art, wie die Regex-Engine verwendet wird. Das Definieren eines regulären Ausdrucks beinhaltet das enge Verbinden der Engine für reguläre Ausdrücke mit einem Muster für reguläre Ausdrücke. Dieser Kopplungsprozess ist teuer, unabhängig davon, ob ein Regex-Objekt instanziiert wird, indem er seinen Konstruktor über ein Muster mit regulären Ausdrücken oder durch Aufrufen einer statischen Methode das Muster des regulären Ausdrucks und die zu analysierende Zeichenfolge übergibt.

Hinweis

Eine ausführliche Erläuterung der Leistungsauswirkungen der Verwendung interpretierter und kompilierter regulärer Ausdrücke finden Sie im Blogbeitrag Optimieren der Leistung regulärer Ausdrücke, Teil II: Steuern der Rückverfolgung.

Sie können die Engine für reguläre Ausdrücke mit einem bestimmten Muster für reguläre Ausdrücke verknüpfen und die Engine dann verwenden, um den Text auf verschiedene Weise abzugleichen:

  • Sie können eine statische Methode für Musterübereinstimmungen aufrufen, z. B. Regex.Match(String, String). Bei dieser Methode ist die Instanziierung des Objekts eines regulären Ausdrucks nicht erforderlich.

  • Sie können ein Regex-Objekt instanziieren und eine Instanzmethode für den Musterabgleich eines interpretierten regulären Ausdrucks aufrufen. Dies ist die Standardmethode zum Binden der Engine für reguläre Ausdrücke an das Muster eines regulären Ausdrucks. Das Ergebnis tritt ein, wenn ein Regex-Objekt ohne ein options-Argument instanziiert wird, das das Compiled-Flag beinhaltet.

  • Sie können ein Regex-Objekt instanziieren und eine Instanzmethode für Musterübereinstimmungen eines quellengenerierten regulären Ausdrucks aufrufen. Diese Technik wird in den meisten Fällen empfohlen. Platzieren Sie dazu das GeneratedRegexAttribute-Attribut auf eine partielle Methode, die Regex zurückgibt.

  • Sie können ein Regex-Objekt instanziieren und eine Instanzmethode für Musterübereinstimmungen eines kompilierten regulären Ausdrucks aufrufen. Objekte regulärer Ausdrücke stellen kompilierte Muster dar, wenn ein Regex-Objekt mit einem options-Argument instanziiert wird, das das Compiled-Flag beinhaltet.

Die Art und Weise, wie Sie Methoden für den Abgleich regulärer Ausdrücke aufrufen, kann sich auf die Leistung Ihrer Anwendung auswirken. In den folgenden Abschnitten wird erläutert, wann statische Methodenaufrufe, quellengenerierte reguläre Ausdrücke, interpretierte reguläre Ausdrücke und kompilierte reguläre Ausdrücke verwendet werden sollen, um die Leistung Ihrer Anwendung zu verbessern.

Wichtig

Die Art des Methodenaufrufs (statisch, interpretiert, quellengeneriert, kompiliert) wirkt sich auf die Leistung aus, wenn ein regulärer Ausdruck wiederholt in Methodenaufrufen verwendet wird oder wenn eine Anwendung umfassenden Gebrauch von Objekten regulärer Ausdrücke macht.

Statische reguläre Ausdrücke

Statische Methoden für reguläre Ausdrücke werden als Alternative zum wiederholten Instanziieren eines Objekts für reguläre Ausdrücke mit demselben regulären Ausdruck empfohlen. Im Gegensatz zu Mustern regulärer Ausdrücke, die von Objekten regulärer Ausdrücke verwendet werden, werden von der Engine für reguläre Ausdrücke entweder die Vorgangscodes (opcodes) oder die kompilierte CIL (Common Intermediate Language) aus Mustern, die in statischen Methodenaufrufen verwendet werden, intern zwischengespeichert.

Beispielsweise ruft ein Ereignishandler häufig eine andere Methode auf, um Benutzereingaben zu überprüfen. Dieses Beispiel wird im folgenden Code gezeigt, in dem das Click-Ereignis eines Button-Steuerelements verwendet wird, um eine Methode namens IsValidCurrency aufzurufen, die überprüft, ob die Benutzer*innen ein Währungssymbol gefolgt von mindestens einer Dezimalziffer eingegeben haben.

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

Eine ineffiziente Implementierung der IsValidCurrency-Methode wird im folgenden Beispiel gezeigt:

Hinweis

Bei jedem Methodenaufruf wird ein Regex-Objekt mit demselben Muster erneut instanziiert. Dies bedeutet wiederum, dass das Muster für reguläre Ausdrücke bei jedem Aufruf der Methode erneut kompiliert werden muss.

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

Sie sollten den obigen ineffizienten Code durch einen Aufruf der statischen Regex.IsMatch(String, String)-Methode ersetzen. Durch diesen Ansatz entfällt die Notwendigkeit, jedes Mal ein Regex-Objekt zu instanziieren, wenn Sie eine Methode für den Musterabgleich aufrufen möchten. Außerdem kann die Engine für reguläre Ausdrücke eine kompilierte Version des regulären Ausdrucks aus ihrem Cache abrufen.

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

Standardmäßig werden die letzten 15 zuletzt verwendeten statischen Muster für reguläre Ausdrücke zwischengespeichert. Für Anwendungen, die eine größere Anzahl von zwischengespeicherten statischen regulären Ausdrücken benötigen, kann die Größe des Caches durch Festlegen der Regex.CacheSize-Eigenschaft angepasst werden.

Der in diesem Beispiel verwendete reguläre Ausdruck \p{Sc}+\s*\d+ überprüft, ob die Eingabezeichenfolge ein Währungssymbol und mindestens eine Dezimalzahl enthält. Das Muster ist wie in der folgenden Tabelle gezeigt definiert:

Muster BESCHREIBUNG
\p{Sc}+ Übereinstimmung mit mindestens einem Zeichen aus der Kategorie „Unicode-Symbol, Währung“.
\s* Übereinstimmung mit 0 (null) oder mehr Leerzeichen.
\d+ Übereinstimmung mit einer oder mehreren Dezimalzahlen.

Interpretiert im Vergleich zu quellengenerierten im Vergleich zu kompilierten regulären Ausdrücken

Muster für reguläre Ausdrücke, die nicht durch Angabe der Compiled-Option an die Engine für reguläre Ausdrücke gebunden sind, werden interpretiert. Wenn ein Objekt für reguläre Ausdrücke instanziiert wird, konvertiert die Engine für reguläre Ausdrücke den regulären Ausdruck in einen Satz von Operationscodes. Beim Aufrufen einer Instanzmethode werden die Vorgangscodes in CIL konvertiert und vom JIT-Compiler ausgeführt. Wenn eine statische Methode für reguläre Ausdrücke aufgerufen und der reguläre Ausdruck nicht im Cache gefunden wird, konvertiert die Engine für reguläre Ausdrücke den regulären Ausdruck ebenfalls in einen Satz von Vorgangscodes und speichert diese im Cache. Dann werden diese Vorgangscodes in CIL konvertiert, damit der JIT-Compiler sie ausführen kann. Interpretierte reguläre Ausdrücke reduzieren die Ladezeit, führen aber zu einer langsameren Ausführungszeit. Ihre Verwendung empfiehlt sich aufgrund dieses Prozesses vor allem dann, wenn der reguläre Ausdruck in einer kleinen Anzahl von Methodenaufrufen verwendet wird oder wenn die genaue Anzahl von Aufrufen von Methoden für reguläre Ausdrücke zwar unbekannt, jedoch erwartungsgemäß niedrig ist. Wenn die Anzahl der Methodenaufrufe zunimmt, wird die durch die kürzere Startzeit erzielte Leistungssteigerung durch die langsamere Ausführungsgeschwindigkeit wieder ausgeglichen.

Muster für reguläre Ausdrücke, die durch Angabe der Compiled-Option an die Engine für reguläre Ausdrücke gebunden sind, werden kompiliert. Wenn also ein Objekt für reguläre Ausdrücke instanziiert oder eine statische Methode für reguläre Ausdrücke aufgerufen wird und der reguläre Ausdruck nicht im Cache gefunden wird, konvertiert die Engine für reguläre Ausdrücke den regulären Ausdruck in einen vorläufigen Satz von Vorgangscodes. Diese Codes werden dann in CIL konvertiert. Wenn eine Methode aufgerufen wird, führt der JIT-Compiler die CIL aus. Im Gegensatz zu interpretierten regulären Ausdrücken erhöhen kompilierte reguläre Ausdrücke die Startzeit. Einzelne Methoden für Musterübereinstimmungen werden aber schneller ausgeführt. Dadurch vergrößert sich der Leistungsvorteil, der sich aus dem Kompilieren eines regulären Ausdrucks ergibt, relativ zur Anzahl der aufgerufenen Methoden für reguläre Ausdrücke.

Muster für reguläre Ausdrücke, die über das Randsteuerelement einer Methode, die Regex mit dem Attribut GeneratedRegexAttribute an das Modul für reguläre Ausdrücke gebunden zurückgibt, werden quellengeneriert. Der Quellgenerator, der an den Compiler anbindet, gibt als C#-Code eine benutzerdefinierte, von Regex abgeleitete Implementierung mit Logik aus, die dem entspricht, was RegexOptions.Compiled in CIL emittiert. Sie erhalten alle Durchsatzleistungsvorteile von RegexOptions.Compiled (tatsächlich sogar noch mehr) sowie die Startvorteile von Regex.CompileToAssembly, aber ohne die Komplexität von CompileToAssembly. Die ausgegebene Quelle ist Teil Ihres Projekts, was bedeutet, dass sie auch mühelos angezeigt und debuggt werden kann.

Zusammenfassend empfehlen wir Folgendes:

  • Verwenden Sie interpretierte reguläre Ausdrücke, wenn Sie reguläre Ausdrucksmethoden mit einem bestimmten regulären Ausdruck relativ selten aufrufen.
  • Verwenden Sie quellengenerierte reguläre Ausdrücke, wenn Sie Regex in C#-Argumenten verwenden, die zur Kompilierungszeit bekannt sind, und Sie einen bestimmten regulären Ausdruck relativ häufig verwenden.
  • Verwenden Sie kompilierte reguläre Ausdrücke, wenn Sie reguläre Ausdrücke mit einem bestimmten regulären Ausdruck relativ häufig aufrufen und .NET 6 oder eine frühere Version verwenden.

Es ist schwierig, den genauen Punkt zu ermitteln, an dem die langsameren Ausführungsgeschwindigkeiten interpretierter regulärer Ausdrücke gegenüber ihren reduzierten Startzeitgewinnen überwiegen. Es ist auch schwierig, den Punkt zu ermitteln, bei dem die langsameren Startzeiten der quellengenerierten oder kompilierten regulären Ausdrücke gegenüber ihren schnelleren Ausführungsgeschwindigkeiten überwiegen. Es hängt von verschiedenen Faktoren ab, z. B. der Komplexität des regulären Ausdrucks und den spezifischen verarbeiteten Daten. Um zu ermitteln, welche regulären Ausdrücke die beste Leistung für Ihr bestimmtes Anwendungsszenario bieten, können Sie die Stopwatch-Klasse verwenden, um ihre Ausführungszeiten zu vergleichen.

Im folgenden Beispiel wird die Leistung von kompilierten, quellgenerierten und interpretierten regulären Ausdrücken beim Lesen der ersten 10 Sätze und beim Lesen aller Sätze im Text von William D. Guthries Magna Carta and Other Addresses verglichen. Die Ausgabe im Beispiel zeigt: Wenn nur zehn Aufrufe von Methoden für den Abgleich regulärer Ausdrücke erfolgen, bietet ein interpretierter oder quellengenerierter regulärer Ausdruck eine bessere Leistung als ein kompilierter regulärer Ausdruck. Ein kompilierter regulärer Ausdruck bietet jedoch die bessere Leistung bei vielen Aufrufen (in diesem Fall über 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
*/

Das im Beispiel verwendete Muster für reguläre Ausdrücke, \b(\w+((\r?\n)|,?\s))*\w+[.?:;!], ist wie in der folgenden Tabelle gezeigt definiert:

Muster Beschreibung
\b Der Vergleich beginnt an einer Wortgrenze.
\w+ Übereinstimmung mit einem oder mehreren Wortzeichen.
(\r?\n)|,?\s) Übereinstimmung mit keinem (null) oder einem Wagenrücklaufzeichen gefolgt von einem Zeilenvorschubzeichen oder keinem (null) oder einem Komma gefolgt von einem Leerzeichen.
(\w+((\r?\n)|,?\s))* Übereinstimmung mit keinem (null) oder mehreren Vorkommen eines oder mehrerer Wortzeichen gefolgt von keinem (null) oder einem Wagenrücklaufzeichen und einem Zeilenvorschubzeichen oder von keinem (null) oder einem Komma gefolgt von einem Leerzeichen.
\w+ Übereinstimmung mit einem oder mehreren Wortzeichen.
[.?:;!] Übereinstimmung mit einem Punkt, Fragezeichen, Doppelpunkt, Semikolon oder Ausrufezeichen.

Steuern der Rückverfolgung

Normalerweise bewegt sich die Engine für reguläre Ausdrücke für den Vergleich mit einem regulären Ausdrucksmuster linear durch eine Eingabezeichenfolge. Wenn jedoch unbestimmte Quantifizierer, z. B. *, + oder ? in einem Muster für reguläre Ausdrücke verwendet werden, gibt die Engine für reguläre Ausdrücke möglicherweise einen Teil der erfolgreichen Teilübereinstimmungen auf und kehrt zu einem zuvor gespeicherten Zustand zurück, um nach einer erfolgreichen Übereinstimmung mit dem gesamten Muster zu suchen. Dieser Prozess wird als Rückverfolgung bezeichnet.

Tipp

Weitere Informationen zur Rückverfolgung finden Sie unter Einzelheiten zum Verhalten regulärer Ausdrücke und Rückverfolgung. Ausführliche Besprechungen der Rückverfolgung finden Sie in den Blogbeiträgen Verbesserungen regulärer Ausdrücke in .NET 7 und Optimieren der Leistung regulärer Ausdrücke.

Durch die Unterstützung des Zurückverfolgens werden reguläre Ausdrücke leistungsstark und flexibel. Außerdem wird die Steuerung der Ausführung der Engine für reguläre Ausdrücke in die Hände der Entwickler von regulären Ausdrücken gelegt. Entwickler sind sich dieser Verantwortung oft nicht bewusst und verwenden die Rückverfolgung falsch oder übermäßig. Dies ist einer der Hauptfaktoren für die Beeinträchtigung der Leistung von regulären Ausdrücken. Im ungünstigsten Fall kann sich die Ausführungszeit für jedes zusätzliche Zeichen in der Eingabezeichenfolge verdoppeln. Bei übermäßiger Verwendung der Rückverfolgung ist es tatsächlich leicht, eine programmatische Entsprechung einer Endlosschleife zu erstellen, wenn die Eingabe fast mit dem Muster für reguläre Ausdrücke übereinstimmt. Für die Verarbeitung einer relativ kurzen Eingabezeichenfolge kann die Engine für regulärer Ausdrücke mehrere Stunden oder sogar Tage brauchen.

Leistungseinbußen bei Anwendungen kommen häufig vor, wenn die Rückverfolgung verwendet wird, obwohl diese für einen Abgleich nicht erforderlich ist. Beispielsweise gleicht der reguläre Ausdruck \b\p{Lu}\w*\b wie in der folgenden Tabelle dargestellt alle Wörter ab, die mit einem Großbuchstaben beginnen:

Muster Beschreibung
\b Der Vergleich beginnt an einer Wortgrenze.
\p{Lu} Übereinstimmung mit einem Großbuchstaben.
\w* Übereinstimmung mit keinem (null) oder mehreren Wortzeichen.
\b Der Vergleich endet an einer Wortgrenze.

Da eine Wortgrenze weder mit einem Wortzeichen identisch noch eine Teilmenge eines Wortzeichens ist, ist es nicht möglich, dass die Engine für reguläre Ausdrücke beim Abgleichen von Wortzeichen eine Wortgrenze überschreitet. Bei diesem regulären Ausdruck kann die Rückverfolgung daher nie zum Gesamterfolg eines Abgleichs beitragen. Sie kann lediglich die Leistung beeinträchtigen, da die Engine für reguläre Ausdrücke gezwungen wird, den Zustand für jeden erfolgreichen vorläufigen Abgleich eines Wortzeichens zu speichern.

Wenn Sie feststellen, dass die Rückverfolgung nicht nötig ist, können Sie sie auf verschiedene Arten deaktivieren:

  • durch Festlegen der RegexOptions.NonBacktracking-Option (eingeführt in .NET 7). Weitere Informationen finden Sie unter Nicht zurückverfolgender Modus.

  • mithilfe des (?>subexpression)-Sprachelements, das als atomische Gruppe bezeichnet wird. Im folgenden Beispiel wird eine Eingabezeichenfolge unter Verwendung von zwei regulären Ausdrücken analysiert. Der erste, \b\p{Lu}\w*\b, beruht auf Rückverfolgung. Der zweite, \b\p{Lu}(?>\w*)\b, deaktiviert die Rückverfolgung. Wie die Ausgabe im Beispiel zeigt, liefern beide dasselbe Ergebnis:

    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 vielen Fällen ist das Zurückverfolgen wichtig, um ein Muster für reguläre Ausdrücke mit dem Eingabetext abzugleichen. Eine übermäßige Rückverfolgung kann jedoch die Leistung erheblich beeinträchtigen und den Eindruck erwecken, dass eine Anwendung nicht mehr reagiert. Dieses Problem tritt insbesondere dann auf, wenn Quantifizierer geschachtelt sind und der Text, der dem äußeren Teilausdruck entspricht, eine Teilmenge des Textes ist, der dem inneren Teilausdruck entspricht.

Warnung

Vermeiden Sie übermäßige Rückverfolgung, und verwenden Sie außerdem die Timeoutfunktion, um sicherzustellen, dass die Leistung von regulären Ausdrücken nicht zu sehr durch übermäßige Rückverfolgung beeinträchtigt wird. Weitere Informationen finden Sie im Abschnitt Verwenden von Timeoutwerten.

Beispielsweise soll das Muster für reguläre Ausdrücke ^[0-9A-Z]([-.\w]*[0-9A-Z])*\$$ einer Teilenummer entsprechen, die aus mindestens einem alphanumerischen Zeichen besteht. Alle zusätzlichen Zeichen können aus einem alphanumerischen Zeichen, einem Bindestrich, einem Unterstrich oder einem Punkt bestehen. Das letzte Zeichen muss jedoch alphanumerisch sein. Ein Dollarzeichen beendet die Teilenummer. In einigen Fällen kann dieses Muster für reguläre Ausdrücke eine schlechte Leistung aufweisen, da die Quantifizierer geschachtelt sind und der Teilausdruck [0-9A-Z] eine Teilmenge des Teilausdrucks [-.\w]* ist.

In diesen Fällen können Sie die Leistung regulärer Ausdrücke optimieren, indem Sie die geschachtelten Quantifizierer entfernen und den äußeren Teilausdruck durch eine Lookahead- oder Lookbehindassertion mit einer Breite von 0 ersetzen. Lookahead- und Lookbehindassertionen sind Anker. Sie bewegen nicht den Mauszeiger in der Eingabezeichenfolge, sondern überprüfen in Vorwärts- bzw. Rückwärtsrichtung, ob eine bestimmte Bedingung erfüllt ist. Beispielsweise kann der reguläre Ausdruck für die Teilenummer wie folgt umgeschrieben werden: ^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$. Dieses Muster für reguläre Ausdrücke ist wie in der folgenden Tabelle gezeigt definiert:

Muster Beschreibung
^ Beginnt den Vergleich am Anfang der Eingabezeichenfolge.
[0-9A-Z] Übereinstimmung mit einem alphanumerischen Zeichen. Die Teilenummer muss aus mindestens diesem Zeichen bestehen.
[-.\w]* Übereinstimmung mit keinem oder mehreren Vorkommen eines beliebigen Wortzeichens, eines Bindestrichs oder eines Punkts.
\$ Übereinstimmung mit einem Dollarzeichen.
(?<=[0-9A-Z]) Lookbehindüberprüfung für das beendende Dollarzeichen, um sicherzustellen, dass das vorherige Zeichen alphanumerisch ist.
$ Ende des Abgleichs am Ende der Eingabezeichenfolge.

Im folgenden Beispiel wird die Verwendung dieses regulären Ausdrucks zum Abgleichen eines Arrays veranschaulicht, das mögliche Teilenummern enthält:

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.

Die Sprache für reguläre Ausdrücke in .NET beinhaltet die folgenden Sprachelemente, die Sie verwenden können, um geschachtelte Quantifizierer zu vermeiden. Weitere Informationen finden Sie unter Gruppierungskonstrukte.

Sprachelement Beschreibung
(?= subexpression ) Positives Lookahead mit einer Breite von 0. Lookaheadüberprüfung für die aktuelle Position, um zu ermitteln, ob subexpression mit der Eingabezeichenfolge übereinstimmt.
(?! subexpression ) Negatives Lookahead mit einer Breite von 0. Lookaheadüberprüfung für die aktuelle Position, um zu ermitteln, ob subexpression nicht mit der Eingabezeichenfolge übereinstimmt.
(?<= subexpression ) Positives Lookbehind mit einer Breite von 0. Lookbehindüberprüfung für die aktuelle Position, um zu ermitteln, ob subexpression mit der Eingabezeichenfolge übereinstimmt.
(?<! subexpression ) Negatives Lookbehind mit einer Breite von 0. Lookbehindüberprüfung für die aktuelle Position, um zu ermitteln, ob subexpression nicht mit der Eingabezeichenfolge übereinstimmt.

Verwenden von Timeoutwerten

Wenn Ihre regulären Ausdrücke Eingaben verarbeiten, die annähernd mit dem Muster des regulären Ausdrucks übereinstimmen, wird häufig übermäßige Rückverfolgung verwendet. Dies beeinträchtigt die Leistung signifikant. Wägen Sie die Verwendung der Rückverfolgung sorgfältig ab, testen Sie den regulären Ausdruck mit fast übereinstimmenden Eingaben, und legen Sie außerdem immer einen Timeoutwert fest, um die Beeinträchtigung durch übermäßige Rückverfolgung zu minimieren.

Das Timeoutintervall des regulären Ausdrucks definiert den Zeitraum bis zum Timeout, in dem die Engine für reguläre Ausdrücke nach einer einzelnen Übereinstimmung sucht, bevor ein Timeout erfolgt. Abhängig vom Muster des regulären Ausdrucks und dem Eingabetext kann die Ausführungszeit das angegebene Timeoutintervall überschreiten, es wird jedoch nicht mehr Zeit für die Rückverfolgung aufgewendet als das angegebene Timeoutintervall. Das Standardtimeoutintervall ist Regex.InfiniteMatchTimeout, d. h. für den regulären Ausdruck erfolgt kein Timeout. Sie können diesen Wert folgendermaßen überschreiben und ein Timeoutintervall definieren:

Wenn Sie ein Timeoutintervall definiert haben und bis zum Ende dieses Intervalls keine Übereinstimmung gefunden wird, löst die Methode des regulären Ausdrucks eine RegexMatchTimeoutException-Ausnahme aus. In Ihrem Ausnahmehandler können Sie festlegen, dass der Abgleich mit einem längeren Timeoutintervall wiederholt wird, dass der Versuch abgebrochen und davon ausgegangen wird, dass keine Übereinstimmung vorhanden ist, oder dass der Versuch abgebrochen und die Ausnahmeinformationen für eine zukünftige Analyse protokolliert werden.

Im folgenden Beispiel wird eine GetWordData-Methode definiert, die einen regulären Ausdruck mit einem Timeoutintervall von 350 Millisekunden instanziiert, um für ein Textdokument die Anzahl der Wörter und die durchschnittliche Anzahl der Zeichen pro Wort zu berechnen. Wenn ein Timout für den entsprechenden Vorgang auftritt, wird das Timeoutintervall um 350 Millisekunden erhöht und das Regex-Objekt erneut instanziiert. Wenn das neue Timeoutintervall 1 Sekunde übersteigt, löst die Methode die Ausnahme erneut für den Aufrufer aus.

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

Erfassungen nur bei Bedarf

Reguläre Ausdrücke in .NET unterstützen Gruppierungskonstrukte, mit denen Sie ein Muster für reguläre Ausdrücke in einem Teilausdruck oder mehreren Teilausdrücken gruppieren können. Die am häufigsten verwendeten Gruppierungskonstrukte in der .NET-Sprache für reguläre Ausdrücke sind (Teilausdruck) zum Definieren einer nummerierten Erfassungsgruppe und (?<Name>Teilausdruck) zum Definieren einer benannten Erfassungsgruppe. Gruppierungskonstrukte sind wichtig für das Erstellen von Rückverweisen und für das Definieren eines Teilausdrucks, auf den ein Quantifizierer angewendet wird.

Die Verwendung dieser Sprachelemente hat jedoch auch Nachteile. Sie führen dazu, dass das von der GroupCollection-Eigenschaft zurückgegebene Match.Groups-Objekt mit den neuesten unbenannten oder benannten Erfassungen aufgefüllt wird. Wenn ein einzelnes Gruppierungskonstrukt mehrere Teilzeichenfolgen in der Eingabezeichenfolge erfasst hat, wird auch das von der Group.Captures-Eigenschaft einer bestimmten Erfassungsgruppe zurückgegebene CaptureCollection-Objekt mit mehreren Capture-Objekten aufgefüllt.

Häufig werden Gruppierungskonstrukte nur in einem regulären Ausdruck verwendet, damit Quantifizierer auf sie angewendet werden können. Die von diesen Teilausdrücken erfassten Gruppen werden später nicht verwendet. Beispielsweise soll der reguläre Ausdruck \b(\w+[;,]?\s?)+[.?!] einen vollständigen Satz erfassen. In der folgenden Tabelle werden die Sprachelemente in diesem regulären Ausdrucksmuster und ihre Auswirkungen auf die Match- und Match.Groups-Auflistungen des Group.Captures-Objekts beschrieben:

Muster Beschreibung
\b Der Vergleich beginnt an einer Wortgrenze.
\w+ Übereinstimmung mit einem oder mehreren Wortzeichen.
[;,]? Übereinstimmung mit keinem (null) oder einem Komma oder Semikolon.
\s? Übereinstimmung mit keinem (null) oder einem Leerzeichen.
(\w+[;,]?\s?)+ Übereinstimmung mit einem oder mehreren Vorkommen eines oder mehrerer Wortzeichen gefolgt von einem optionalen Komma oder Semikolon gefolgt von einem optionalen Leerzeichen. Dieses Muster definiert die erste Erfassungsgruppe. Diese ist erforderlich, damit die Kombination mehrerer Wortzeichen (d. h. ein Wort) gefolgt von einem optionalen Interpunktionszeichen wiederholt wird, bis die Engine für reguläre Ausdrücke das Ende eines Satzes erreicht.
[.?!] Übereinstimmung mit einem Punkt, Fragezeichen oder Ausrufezeichen.

Wie im folgenden Beispiel gezeigt, werden das GroupCollection-Objekt und das CaptureCollection-Objekt mit Erfassungen aus der Übereinstimmungssuche aufgefüllt, wenn eine Übereinstimmung gefunden wird. In diesem Fall wird die Erfassungsgruppe (\w+[;,]?\s?) angegeben, damit der +-Quantifizierer darauf angewendet werden kann, wodurch das Muster für reguläre Ausdrücke Übereinstimmungen für jedes Wort in einem Satz erfassen kann. Andernfalls würde es mit dem letzten Wort in einem Satz abgeglichen werden.

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.

Wenn Sie Teilausdrücke nur verwenden, um Quantifizierer darauf anzuwenden, und den erfassten Text nicht benötigen, sollten Sie Gruppenerfassungen deaktivieren. Zum Beispiel verhindert das Sprachelement (?:subexpression), dass die Gruppe, auf die es angewendet wird, übereinstimmende Teilzeichenfolgen erfasst. Im folgenden Beispiel wird das Muster eines regulären Ausdrucks aus dem vorherigen Beispiel in \b(?:\w+[;,]?\s?)+[.?!] geändert. Wie die Ausgabe im Beispiel zeigt, wird dadurch verhindert, dass die Engine für reguläre Ausdrücke die GroupCollection- und CaptureCollection-Auflistungen auffüllt:

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.

Erfassungen können Sie auf eine der folgenden Arten deaktivieren:

  • Verwenden Sie das Sprachelement (?:subexpression). Dieses Element verhindert die Erfassung übereinstimmender Teilzeichenfolge in der Gruppe, auf die es angewendet wird. Die Erfassung von Teilzeichenfolgen in geschachtelten Gruppen wird dadurch nicht deaktiviert.

  • Verwenden Sie die ExplicitCapture-Option. Diese Option deaktiviert alle unbenannten oder impliziten Erfassungen im Muster für reguläre Ausdrücke. Wenn Sie diese Option verwenden, können nur Teilzeichenfolgen erfasst werden, die mit benannten Gruppen übereinstimmen, die mit dem Sprachelement (?<name>subexpression) definiert wurden. Das ExplicitCapture-Flag kann an den options-Parameter eines Regex-Klassenkonstruktors oder an den options-Parameter einer statischen Regex Übereinstimmungsmethode übergeben werden.

  • Verwenden Sie die n-Option im Sprachelement (?imnsx). Diese Option deaktiviert alle unbenannten oder impliziten Erfassungen ab dem Punkt im Muster für reguläre Ausdrücke, an dem das Element erscheint. Erfassungen werden entweder bis zum Ende des Musters oder so lange deaktiviert, bis die (-n)-Option unbenannte oder implizite Erfassungen aktiviert. Weitere Informationen finden Sie unter Verschiedene Konstrukte.

  • Verwenden Sie die n-Option im Sprachelement (?imnsx:subexpression). Diese Option deaktiviert alle unbenannten oder impliziten Erfassungen in subexpression. Erfassungen von allen unbenannten oder impliziten geschachtelten Erfassungsgruppen werden ebenfalls deaktiviert.

Threadsicherheit

Die Regex-Klasse selbst ist threadsicher und nicht änderbar (schreibgeschützt). Regex-Objekte können also in jedem Thread erzeugt und von mehreren Threads gemeinsam genutzt werden. Übereinstimmende Methoden können von jedem Thread aufgerufen werden und ändern keinen globalen Zustand.

Ergebnisobjekte (Match und MatchCollection), die von Regex zurückgegeben werden, sollten jedoch in einem einzelnen Thread verwendet werden. Obwohl viele dieser Objekte logisch nicht änderbar sind, können ihre Implementierungen die Berechnung einiger Ergebnisse zur Verbesserung der Leistung verzögern. Daher müssen Aufrufer den Zugriff darauf serialisieren.

Wenn Regex-Ergebnisobjekte in mehreren Threads gemeinsam genutzt werden müssen, können diese Objekte durch den Aufruf ihrer synchronisierten Methoden in threadsichere Instanzen konvertiert werden. Mit Ausnahme von Enumeratoren sind alle Klassen für reguläre Ausdrücke threadsicher oder können von einer synchronisierten Methode in threadsichere Objekte konvertiert werden.

Enumeratoren sind die einzige Ausnahme. Sie müssen Aufrufe von Sammlungsenumeratoren serialisieren. Die Regel ist, dass für eine Sammlung, die in mehr als einem Thread gleichzeitig als Enumeration verwendet werden kann, die Enumeratormethoden im Stammobjekt der Sammlung, die der Enumerator durchläuft, synchronisiert werden müssen.

Titel Beschreibung
Einzelheiten zum Verhalten regulärer Ausdrücke Überprüft die Implementierung der Engine für reguläre Ausdrücke in .NET. Schwerpunkt dieses Artikels ist die Flexibilität regulärer Ausdrücke. Außerdem wird die Verantwortung der Entwickler*innen erläutert, die effiziente und stabile Ausführung der Engine für reguläre Ausdrücke sicherzustellen.
Backtracking Erläutert die Rückverfolgung und deren Auswirkungen auf die Leistung von regulären Ausdrücken. Zudem werden Sprachelemente beschrieben, die Alternativen zum Zurückverfolgen bieten.
Sprachelemente für reguläre Ausdrücke – Kurzübersicht Beschreibt die Elemente der Sprache für reguläre Ausdrücke in .NET und enthält Links zu ausführlichen Dokumentationen für jedes Sprachelement.