Поделиться через


Рекомендации по регулярным выражениям в .NET

Подсистема регулярных выражений в .NET — это мощный полнофункциональный инструмент, который обрабатывает текст на основе совпадений шаблонов, а не сравнивает и сопоставляет литеральный текст. В большинстве случаев он выполняет сопоставление шаблонов быстро и эффективно. Однако в некоторых случаях подсистема регулярных выражений может оказаться медленной. В крайних случаях он может даже перестать отвечать, поскольку он обрабатывает относительно небольшие входные данные в течение нескольких часов или даже дней.

В этой статье описаны некоторые рекомендации, которые разработчики могут принять, чтобы обеспечить оптимальную производительность своих регулярных выражений.

Предупреждение

При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злоумышленник может предоставить входные данные RegularExpressions, что приводит к атаке типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.

Рассмотрим источник входных данных

Как правило, регулярные выражения могут принимать два типа входных данных: ограниченные или не ограниченные. Ограниченные входные данные — это текст, исходящий из известного или надежного источника и соответствующий предварительно определенному формату. Неуверенные входные данные — это текст, исходящий из ненадежного источника, например веб-пользователя, и может не соответствовать предопределенным или ожидаемым форматам.

Шаблоны регулярных выражений часто записываются в соответствие с допустимыми входными данными. То есть разработчики проверяют текст, который они хотят сопоставить, а затем записывают шаблон регулярного выражения, соответствующий ему. Затем разработчики определяют, нужно ли скорректировать эту закономерность или более подробно её изучить, тестируя её с использованием нескольких допустимых входных данных. Если шаблон соответствует всем предполагаемым допустимым входным данным, он объявляется готовым к рабочей среде и может быть включен в выпущенное приложение. Этот подход делает шаблон регулярного выражения подходящим для сопоставления ограниченных входных данных. Однако это не делает его подходящим для сопоставления независимых входных данных.

Для обработки неограниченных входных данных регулярное выражение должно эффективно обрабатывать три типа текста.

  • Текст, соответствующий шаблону регулярного выражения.
  • Текст, который не соответствует шаблону регулярного выражения.
  • Текст, который почти соответствует шаблону регулярного выражения.

Последний текстовый тип особенно проблематичен для регулярного выражения, написанного для обработки ограниченных входных данных. Если это регулярное выражение также зависит от обширного обратного отслеживания, обработчик регулярных выражений может тратить чрезмерно много времени (в некоторых случаях много часов или дней) на обработку казалось бы, безобидного текста.

Предупреждение

В следующем примере используется регулярное выражение, которое подвержено чрезмерному обратному отслеживанию и, скорее всего, отклоняет допустимые адреса электронной почты. Его не следует использовать в подпрограмме проверки электронной почты. Если вы хотите регулярное выражение, которое проверяет адреса электронной почты, см. статью "Практическое руководство. Убедитесь, что строки находятся в допустимом формате электронной почты".

Например, рассмотрим часто используемое, но проблематичное регулярное выражение для проверки псевдонима адреса электронной почты. Регулярное выражение ^[0-9A-Z]([-.\w]*[0-9A-Z])*$ записывается для обработки того, что считается допустимым адресом электронной почты. Допустимый адрес электронной почты состоит из буквенно-цифрового символа, за которым следует нулевая или более символов, которые могут быть буквенно-цифровыми, периодами или дефисами. Регулярное выражение должно заканчиваться буквенно-цифровым символом. Однако, как показано в следующем примере, хотя это регулярное выражение легко обрабатывает допустимые входные данные, его производительность неэффективна при обработке почти допустимых входных данных:

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

Как показано в выходных данных предыдущего примера, обработчик регулярных выражений обрабатывает допустимый псевдоним электронной почты примерно в тот же интервал времени независимо от его длины. С другой стороны, если почти допустимый адрес электронной почты имеет более пяти символов, время обработки приблизительно удваивается для каждого дополнительного символа в строке. Таким образом, почти допустимая строка 28 символов займет более часа для обработки, и почти допустимая строка 33 символов займет почти день для обработки.

Так как это регулярное выражение было разработано исключительно с учетом формата входных данных, он не учитывает входные данные, которые не соответствуют шаблону. Это упущение, в свою очередь, может позволить неограниченный ввод данных, который почти соответствует шаблону регулярного выражения, что значительно снижает производительность.

Чтобы решить эту проблему, можно выполнить следующее:

  • При разработке шаблона следует учитывать, как бектрекинг может повлиять на производительность движка регулярных выражений, особенно если регулярное выражение предназначено для обработки неограниченных входных данных. Дополнительные сведения см. в разделе "Управление обратным поиском".

  • Тщательно протестируйте регулярное выражение с помощью недопустимых, близких к допустимым и допустимых входных данных. Rex можно использовать для случайного создания входных данных для определенного регулярного выражения. Rex — это средство исследования регулярных выражений из Microsoft Research.

Обеспечьте правильное создание экземпляра объекта

В самом центре объектной модели регулярных выражений .NET находится класс System.Text.RegularExpressions.Regex, представляющий движок регулярных выражений. Зачастую наиболее значимый фактор, влияющий на производительность регулярных выражений, — это способ использования Regex движка. Определение регулярного выражения включает тесное связывание обработчика регулярных выражений с шаблоном регулярных выражений. Этот процесс связывания является дорогостоящим, будь то создание экземпляра объекта Regex путем передачи в конструктор шаблона регулярного выражения или вызов статического метода путем передачи в него шаблона регулярного выражения и строки для анализа.

Замечание

Подробное обсуждение влияния на производительность использования интерпретированных и скомпилированных регулярных выражений см. в записи блога Оптимизация производительности регулярных выражений, часть II: Контроль за бэктрекингом.

Подсистему регулярных выражений можно сочетать с определенным шаблоном регулярного выражения, а затем использовать обработчик для сопоставления текста несколькими способами:

  • Можно вызвать статический метод сопоставления шаблонов, например Regex.Match(String, String). Этот метод не требует создания экземпляра объекта регулярного выражения.

  • Можно создать экземпляр Regex объекта и вызвать метод сопоставления шаблонов экземпляра интерпретированного регулярного выражения, который является методом по умолчанию для привязки обработчика регулярных выражений к шаблону регулярного выражения. Это происходит, когда экземпляр объекта Regex создается без аргумента options, который включает флаг Compiled.

  • Можно создать экземпляр Regex объекта и вызвать метод сопоставления шаблонов экземпляра исходного регулярного выражения. Этот метод рекомендуется в большинстве случаев. Чтобы сделать это, поместите атрибут GeneratedRegexAttribute в частичный метод, который возвращает Regex.

  • Можно создать экземпляр Regex объекта и вызвать метод сопоставления шаблонов экземпляра скомпилированного регулярного выражения. Когда создается экземпляр объекта Regex с аргументом options, включающим флаг Compiled, эти объекты регулярных выражений представляют собой скомпилированные шаблоны.

Конкретный способ вызова методов сопоставления регулярных выражений может повлиять на производительность приложения. В следующих разделах рассматривается использование вызовов статических методов, исходных регулярных выражений, интерпретированных регулярных выражений и скомпилированных регулярных выражений для повышения производительности приложения.

Это важно

Форма вызова метода (статический, интерпретируемый, исходный, скомпилированный) влияет на производительность, если одно регулярное выражение многократно используется в вызовах методов или если приложение использует широкое использование объектов регулярного выражения.

Статические регулярные выражения

Рекомендуется использовать методы статических регулярных выражений в качестве альтернативы многократному созданию экземпляра объекта регулярного выражения с одним и тем же регулярным выражением. В отличие от шаблонов регулярных выражений, используемых объектами регулярных выражений, коды операций (opcodes) или скомпилированный общий промежуточный язык (CIL) из шаблонов, используемых в статических вызовах методов, кэшируются внутренне обработчиком регулярных выражений.

Например, обработчик событий часто вызывает другой метод для проверки входных данных пользователя. Этот пример отражается в следующем коде, в котором Button событие элемента управления Click используется для вызова метода с именем IsValidCurrency, который проверяет, ввел ли пользователь символ валюты, за которым следует по крайней мере одна десятичная цифра.

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

Неэффективная реализация IsValidCurrency метода показана в следующем примере:

Замечание

Каждый метод вызывает повторное создание Regex объекта с одинаковым шаблоном. Это, в свою очередь, означает, что шаблон регулярного выражения должен быть перекомпилирован при каждом вызове метода.

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

Необходимо заменить предыдущий неэффективный код вызовом статического Regex.IsMatch(String, String) метода. Этот подход устраняет необходимость создания экземпляра Regex объекта при каждом вызове метода сопоставления шаблонов и позволяет обработчику регулярных выражений получить скомпилированную версию регулярного выражения из кэша.

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

По умолчанию кэшируются последние 15 последних используемых шаблонов статических регулярных выражений. Для приложений, требующих большего количества кэшированных статических регулярных выражений, размер кэша можно изменить, задав Regex.CacheSize свойство.

Регулярное выражение \p{Sc}+\s*\d+ , используемое в этом примере, проверяет, что входная строка имеет символ валюты и по крайней мере одну десятичную цифру. Шаблон определен, как показано в следующей таблице:

Рисунок Описание
\p{Sc}+ Соответствует одному или нескольким символам в категории символов Юникода, валюта.
\s* Соответствует нулю или нескольким символам пробела.
\d+ Соответствует одному или нескольким десятичным цифрам.

Интерпретируемые, сгенерированные из исходного кода и скомпилированные регулярные выражения

Шаблоны регулярных выражений, которые не привязаны к обработчику регулярных выражений с помощью спецификации Compiled параметра, интерпретируются. При создании экземпляра объекта регулярного выражения обработчик регулярных выражений преобразует регулярное выражение в набор кодов операций. При вызове метода экземпляра коды операций преобразуются в CIL и выполняются компилятором JIT. Аналогичным образом, когда вызывается метод статического регулярного выражения и не удается найти регулярное выражение в кэше, обработчик регулярных выражений преобразует регулярное выражение в набор кодов операций и сохраняет их в кэше. Затем он преобразует эти коды операций в CIL, чтобы компилятор JIT смог их выполнить. Интерпретированные регулярные выражения сокращают время запуска за счет более медленного выполнения. Из-за этого процесса они лучше всего используются, если регулярное выражение используется в небольшом количестве вызовов методов, или если точное количество вызовов методов регулярного выражения неизвестно, но ожидается, что оно будет небольшим. По мере увеличения числа вызовов методов преимущество от сокращения времени запуска теряется из-за более медленной скорости выполнения.

Шаблоны регулярных выражений, привязанные к обработчику регулярных выражений, с помощью спецификации Compiled параметра компилируются. Поэтому при создании экземпляра объекта регулярного выражения или при вызове метода статического регулярного выражения и не удается найти регулярное выражение в кэше, подсистема регулярных выражений преобразует регулярное выражение в промежуточный набор кодов операций. Затем эти коды преобразуются в CIL. При вызове метода компилятор JIT выполняет CIL. В отличие от интерпретированных регулярных выражений, скомпилированные регулярные выражения увеличивают время запуска, но выполняют отдельные методы сопоставления шаблонов быстрее. В результате преимущество производительности, которое приводит к компиляции регулярного выражения, увеличивается пропорционально количеству вызываемых методов регулярного выражения.

Шаблоны регулярных выражений, связанные с движком регулярных выражений, создаются с помощью украшения метода, возвращающего Regex, атрибутом GeneratedRegexAttribute и сгенерированы на основе исходного кода. Генератор источника, который подключается к компилятору, выдает код C# пользовательской Regexпроизводной реализации с логикой, аналогичной тому, что RegexOptions.Compiled выдает в CIL. Вы получаете все преимущества RegexOptions.Compiled производительности пропускной способности (больше, на самом деле) и преимущества Regex.CompileToAssemblyзапуска, но без сложности CompileToAssembly. Источник, который создается, является частью проекта, что означает, что он также легко просматривается и отлаживаться.

Чтобы свести итоги, рекомендуется выполнить следующие действия.

  • Используйте интерпретированные регулярные выражения при вызове методов регулярного выражения с определенным регулярным выражением относительно редко.
  • Используйте регулярные выражения, сгенерированные исходным кодом, если вы используете C# с аргументами, известными на этапе компиляции, и часто используете определенное регулярное выражение.
  • Используйте скомпилированные регулярные выражения, если вы довольно часто вызываете методы регулярного выражения с определённым регулярным выражением и используете .NET 6 или более раннюю версию.

Трудно определить точное пороговое значение, при котором более медленные скорости выполнения интерпретированных регулярных выражений перевешивают выгоды от снижения времени запуска. Кроме того, трудно определить пороговое значение, при котором более медленное время запуска созданных источником или скомпилированных регулярных выражений перевешивает преимущества от скорости выполнения. Пороговые значения зависят от различных факторов, включая сложность регулярного выражения и конкретных данных, которые он обрабатывает. Чтобы определить, какие регулярные выражения обеспечивают лучшую производительность для конкретного сценария приложения, можно использовать Stopwatch класс для сравнения времени выполнения.

В следующем примере сравнивается производительность скомпилированных, сгенерированных из исходного кода и интерпретированных регулярных выражений при чтении первых 10 предложений и при чтении всех предложений в тексте произведения Уильяма Д. Гатри "Магна Карта и другие выступления". Как показано в выходных данных из примера, при выполнении только 10 вызовов к методам сопоставления регулярных выражений интерпретированное или созданное источником регулярное выражение обеспечивает лучшую производительность, чем скомпилированное регулярное выражение. Однако скомпилированное регулярное выражение обеспечивает более высокую производительность при выполнении большого количества вызовов (в данном случае более 13 000).

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

static readonly HttpClient s_client = new();

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

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

    string text =
            await s_client.GetStringAsync("https://www.gutenberg.org/cache/epub/64197/pg64197.txt");

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

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

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

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

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

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

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

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

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

Шаблон регулярного выражения, используемый в примере, \b(\w+((\r?\n)|,?\s))*\w+[.?:;!]определяется, как показано в следующей таблице:

Рисунок Описание
\b Начинайте сопоставление на границе слова.
\w+ Соответствует одному или нескольким символам слова.
(\r?\n)|,?\s) Соответствует либо нулю, либо одному возврату каретки, за которым следует символ новой строки, либо ноль или одна запятая, за которой следует пробел.
(\w+((\r?\n)|,?\s))* Соответствует нулю или нескольким вхождениям одного или нескольких буквенно-цифровых символов, за которыми следует либо отсутствие, либо один возврат каретки и символ новой строки, либо отсутствие, либо одна запятая и символ пробела.
\w+ Соответствует одному или нескольким символам слова.
[.?:;!] Соответствует точке, вопросительному знаку, двоеточию, точке с запятой или восклицательному знаку.

Взять на себя ответственность за пересмотр решений

Обычно обработчик регулярных выражений использует линейную прогрессию для перемещения по входной строке и сравнения его с шаблоном регулярного выражения. Однако, если неопределенные квантификаторы, такие как *, + и ?, используются в шаблоне регулярного выражения, подсистема регулярных выражений может отказаться от части успешных частичных совпадений и вернуться в ранее сохраненное состояние, чтобы найти успешное совпадение для всего шаблона. Этот процесс называется обратным отслеживанием.

Подсказка

Дополнительные сведения о обратном отслеживании см. в разделе "Сведения о поведении регулярных выражений " и обратном отслеживании. Подробные сведения о обратном отслеживании см. в статьях об улучшениях регулярного выражения в .NET 7 и оптимизации производительности регулярных выражений .

Поддержка обратного отслеживания повышает мощность и гибкость регулярных выражений. Это также возлагает ответственность за управление работой обработчика регулярных выражений на разработчиков регулярных выражений. Поскольку разработчики часто не знают о такой ответственности, их неправильное использование обратного отслеживания или зависимость от чрезмерного обратного отслеживания часто играет наиболее важную роль в снижении производительности регулярных выражений. В худшем случае время выполнения может удваиваться с каждым дополнительным символом во входной строке. На самом деле, используя чрезмерное отслеживание, легко создать программный эквивалент бесконечного цикла, если входные данные почти совпадают с шаблоном регулярного выражения. Обработчик регулярных выражений может занять несколько часов или даже дней для обработки относительно короткой входной строки.

Часто приложения платят штраф за производительность за использование обратного отслеживания, даже если обратная дорожка не является важной для матча. Например, регулярное выражение \b\p{Lu}\w*\b соответствует всем словам, начинающимся с верхнего регистра, как показано в следующей таблице:

Рисунок Описание
\b Начинайте сопоставление на границе слова.
\p{Lu} Соответствует заглавной букве.
\w* Соответствует нулю или нескольким символам слова.
\b Совпадение должно заканчиваться на границе слова.

Так как граница слова не совпадает с символом слова и не является его подмножеством, обработчик регулярных выражений не сможет пересечь границу слова при сопоставлении символов слов. Таким образом, для этого регулярного выражения бэктрекинг никогда не может способствовать успешному завершению любого совпадения. Это может снизить производительность, так как модуль регулярных выражений вынужден сохранять состояние для каждого успешного предварительного совпадения буквенного символа.

Если вы определите, что обратная дорожка не требуется, ее можно отключить несколькими способами:

  • Задавая параметр RegexOptions.NonBacktracking (введённый в .NET 7). Дополнительные сведения см. в режиме без возврата .

  • Используя элемент (?>subexpression) языка, известный как атомарная группа. В следующем примере анализируется входная строка с помощью двух регулярных выражений. Во-первых, \b\p{Lu}\w*\b полагается на откат. Во-вторых, \b\p{Lu}(?>\w*)\b отключает бэктрекинг. Как показано в выходных данных из примера, оба они создают один и тот же результат:

    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
    

Во многих случаях обратная дорожка необходима для сопоставления шаблона регулярного выражения с входным текстом. Однако чрезмерное обратное отслеживание может значительно снизить производительность и создать впечатление, что приложение перестало отвечать. В частности, эта проблема возникает при вложенных квантификаторах, а текст, соответствующий внешнему подтексту, является подмножеством текста, соответствующего внутреннему подтексту.

Предупреждение

Помимо предотвращения чрезмерного бэктрекинга, следует использовать функцию тайм-аута, чтобы убедиться, что чрезмерный бэктрекинг не снижает производительность регулярных выражений. Дополнительные сведения см. в разделе "Использование значений времени ожидания ".

Например, шаблон ^[0-9A-Z]([-.\w]*[0-9A-Z])*\$$ регулярного выражения предназначен для сопоставления номера частей, состоящего по крайней мере из одного буквенно-цифрового символа. Любые дополнительные символы могут состоять из буквенно-цифровых символов, дефиса, подчеркивания или периода, хотя последний символ должен быть буквенно-цифровым. Знак доллара завершает номер части. В некоторых случаях этот шаблон регулярных выражений может иметь низкую производительность, так как квантификаторы вложены, и поскольку подвыражение [0-9A-Z] является подмножеством подвыражения [-.\w]*.

В таких случаях можно оптимизировать производительность регулярных выражений, удалив вложенные квантификаторы и заменив внешнее подвыражение на предвосхищающее или опережающее утверждение. Утверждения Lookahead и Lookbehind выполняют роль привязок. Они не перемещают указатель в входной строке, а вместо этого смотрят вперед или позади, чтобы проверить, соответствует ли указанное условие. Например, регулярное выражение номера части можно переписать как ^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$. Этот шаблон регулярного выражения определен, как показано в следующей таблице:

Рисунок Описание
^ Начало совпадения в начале входной строки.
[0-9A-Z] Найдите буквенно-цифровой символ. Номер части должен состоять по крайней мере из одного символа.
[-.\w]* Совпадение с нулем или более вхождений любого буквенно-цифрового символа, дефиса или точки.
\$ Соответствует знаку доллара.
(?<=[0-9A-Z]) Посмотрите за завершающим знаком доллара, чтобы убедиться, что предыдущий символ буквенно-цифровой.
$ Прекратите поиск в конце входной строки.

В следующем примере показано использование этого регулярного выражения для сопоставления массива, содержащего возможные номера частей:

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.

Язык регулярного выражения в .NET включает следующие языковые элементы, которые можно использовать для устранения вложенных квантификаторов. Дополнительные сведения см. в разделе "Конструкции группирования".

Элемент Language Описание
(?= subexpression ) Положительный внешний вид нулевой ширины. Просматривает текущую позицию, чтобы определить, соответствует ли subexpression входная строка.
(?! subexpression ) Отрицательная проверка вперед с нулевой шириной. Заглядывает за текущую позицию, чтобы определить, не соответствует ли subexpression входной строке.
(?<= subexpression ) Положительный внешний вид нулевой ширины. Просматривает текущую позицию, чтобы определить, соответствует ли subexpression входная строка.
(?<! subexpression ) Отрицательный внешний вид нулевой ширины. Проверяет предыдущую позицию, чтобы определить, не соответствует ли subexpression входная строка.

Используйте значения времени ожидания

Если ваши регулярные выражения обрабатывают входные данные, которые почти соответствуют шаблону регулярного выражения, они могут часто полагаться на чрезмерный бэктрекинг, что значительно влияет на их производительность. Помимо тщательного рассмотрения использования бэктрекинга и тестирования регулярного выражения на близкие к входным данным, всегда следует задать значение времени ожидания, чтобы свести к минимуму эффект чрезмерного бэктрекинга, если это случится.

Интервал времени ожидания регулярного выражения определяет период времени, когда обработчик регулярных выражений будет искать одно совпадение до истечения времени ожидания. В зависимости от шаблона регулярного выражения и входного текста время выполнения может превышать указанный интервал времени ожидания, но он не будет тратить больше времени обратного отслеживания, чем указанный интервал времени ожидания. Интервал тайм-аута по умолчанию — это Regex.InfiniteMatchTimeout, что означает, что регулярное выражение не прервется. Вы можете изменить это значение и задать интервал тайм-аута следующим образом:

  • Вызовите конструктор Regex(String, RegexOptions, TimeSpan), чтобы указать значение времени ожидания при создании экземпляра объекта Regex.

  • Вызовите статический метод сопоставления шаблонов, например Regex.Match(String, String, RegexOptions, TimeSpan) или Regex.Replace(String, String, String, RegexOptions, TimeSpan), который включает matchTimeout параметр.

  • Задайте значение на уровне процесса или домена приложения с помощью кода, например AppDomain.CurrentDomain.SetData("REGEX_DEFAULT_MATCH_TIMEOUT", TimeSpan.FromMilliseconds(100));.

Если вы задали интервал ожидания и совпадение не найдено к концу этого интервала, метод регулярного выражения вызывает RegexMatchTimeoutException исключение. В обработчике исключений можно повторить совпадение с длительным интервалом времени ожидания, отказаться от попытки сопоставления и предположить, что совпадения нет, или отказаться от попытки сопоставления и записать сведения об исключении для дальнейшего анализа.

В следующем примере определяется GetWordData метод, который создает экземпляр регулярного выражения с интервалом времени ожидания в 350 миллисекунд, чтобы вычислить количество слов и среднее число символов в слове в текстовом документе. Если время ожидания операции сопоставления истекло, интервал времени ожидания увеличивается на 350 миллисекунд, а Regex объект повторно создается. Если новый интервал времени ожидания превышает одну секунду, метод повторно выбросит исключение вызываемому коду.

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;

public class TimeoutExample
{
    public static void Main()
    {
        RegexUtilities util = new RegexUtilities();
        string title = "Doyle - The Hound of the Baskervilles.txt";
        try
        {
            var info = util.GetWordData(title);
            Console.WriteLine($"Words:               {info.Item1:N0}");
            Console.WriteLine($"Average Word Length: {info.Item2:N2} characters");
        }
        catch (IOException e)
        {
            Console.WriteLine($"IOException reading file '{title}'");
            Console.WriteLine(e.Message);
        }
        catch (RegexMatchTimeoutException e)
        {
            Console.WriteLine($"The operation timed out after {e.MatchTimeout.TotalMilliseconds:N0} milliseconds");
        }
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

Запись только при необходимости

Регулярные выражения в .NET поддерживают конструкции группирования, которые позволяют группировать шаблон регулярного выражения в одну или несколько вложенных выражений. Наиболее часто используемые конструкции группирования в языке регулярных выражений .NET это (подвыражения), которые определяют номерованную захватывающую группу, и (?<подвыражения имени>, которые определяют именованную захватывающую группу. Конструкции группирования важны для создания обратных ссылок и определения подтекстов, к которым применяется квантификатор.

Однако использование этих языковых элементов имеет затраты. Они приводят к тому, что объект, GroupCollection возвращаемый Match.Groups свойством, заполняется последними неименованными или именованными захватами. Если одна конструкция группирования захватила несколько подстрок во входном тексте, они также заполняют объект CaptureCollection, который возвращает свойство Group.Captures определенной захватывающей группы, несколькими объектами Capture.

Часто конструкции группировки используются в регулярном выражении только таким образом, чтобы к ним могли применяться квантификаторы. Группы, захваченные этими вложенными выражениями, не используются позже. Например, регулярное выражение \b(\w+[;,]?\s?)+[.?!] предназначено для записи всего предложения. В следующей таблице описываются языковые элементы в этом шаблоне регулярных выражений и их влияние на объекты Match и коллекции Match.GroupsGroup.Captures.

Рисунок Описание
\b Начинайте сопоставление на границе слова.
\w+ Соответствует одному или нескольким символам слова.
[;,]? Соответствует нулю или одной из запятых или точек с запятой.
\s? Соответствует нулю или одному символу пробела.
(\w+[;,]?\s?)+ Соответствует одному или нескольким вхождениям одного или нескольких буквенных символов, за которыми следует необязательная запятая или точка с запятой, а затем необязательный пробел. Этот шаблон определяет первую группу записи, которая необходима, чтобы сочетание нескольких символов слов (то есть слово), за которым следует необязательный символ препинания, будет повторяться до тех пор, пока обработчик регулярных выражений не достигнет конца предложения.
[.?!] Соответствует точке, вопросительному знаку или восклицательному знаку.

Как показано в следующем примере, когда находится совпадение, оба объекта GroupCollection и CaptureCollection заполняются захватами из этого совпадения. В этом случае группа (\w+[;,]?\s?) захвата существует для того, чтобы к ней можно было применить квантификатор +, что позволяет шаблону регулярного выражения соответствовать каждому слову в предложении. В противном случае оно будет соответствовать последнему слову в предложении.

using System;
using System.Text.RegularExpressions;

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

        foreach (Match match in Regex.Matches(input, pattern))
        {
            Console.WriteLine($"Match: '{match.Value}' at index {match.Index}.");
            int grpCtr = 0;
            foreach (Group grp in match.Groups)
            {
                Console.WriteLine($"   Group {grpCtr}: '{grp.Value}' at index {grp.Index}.");
                int capCtr = 0;
                foreach (Capture cap in grp.Captures)
                {
                    Console.WriteLine($"      Capture {capCtr}: '{cap.Value}' at {cap.Index}.");
                    capCtr++;
                }
                grpCtr++;
            }
            Console.WriteLine();
        }
    }
}
// The example displays the following output:
//       Match: 'This is one sentence.' at index 0.
//          Group 0: 'This is one sentence.' at index 0.
//             Capture 0: 'This is one sentence.' at 0.
//          Group 1: 'sentence' at index 12.
//             Capture 0: 'This ' at 0.
//             Capture 1: 'is ' at 5.
//             Capture 2: 'one ' at 8.
//             Capture 3: 'sentence' at 12.
//
//       Match: 'This is another.' at index 22.
//          Group 0: 'This is another.' at index 22.
//             Capture 0: 'This is another.' at 22.
//          Group 1: 'another' at index 30.
//             Capture 0: 'This ' at 22.
//             Capture 1: 'is ' at 27.
//             Capture 2: 'another' at 30.
Imports System.Text.RegularExpressions

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

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

При использовании подвыражений только для применения квантификаторов к ним и если вас не интересует зафиксированный текст, следует отключить захват групп. Например, языковой (?:subexpression) элемент запрещает группе, к которой она применяется, записывать соответствующие подстроки. В следующем примере шаблон регулярного выражения из предыдущего примера изменяется на \b(?:\w+[;,]?\s?)+[.?!]. Как показано в выходных данных, модуль регулярных выражений не заполняет GroupCollection и CaptureCollection коллекции:

using System;
using System.Text.RegularExpressions;

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

        foreach (Match match in Regex.Matches(input, pattern))
        {
            Console.WriteLine($"Match: '{match.Value}' at index {match.Index}.");
            int grpCtr = 0;
            foreach (Group grp in match.Groups)
            {
                Console.WriteLine($"   Group {grpCtr}: '{grp.Value}' at index {grp.Index}.");
                int capCtr = 0;
                foreach (Capture cap in grp.Captures)
                {
                    Console.WriteLine($"      Capture {capCtr}: '{cap.Value}' at {cap.Index}.");
                    capCtr++;
                }
                grpCtr++;
            }
            Console.WriteLine();
        }
    }
}
// The example displays the following output:
//       Match: 'This is one sentence.' at index 0.
//          Group 0: 'This is one sentence.' at index 0.
//             Capture 0: 'This is one sentence.' at 0.
//
//       Match: 'This is another.' at index 22.
//          Group 0: 'This is another.' at index 22.
//             Capture 0: 'This is another.' at 22.
Imports System.Text.RegularExpressions

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

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

Вы можете отключить записи одним из следующих способов:

  • Используйте элемент языка (?:subexpression). Этот элемент предотвращает захват совпавших подстрок в группе, к которой он применяется. Он не отключает захват подстрок в любых вложенных группах.

  • Использовать параметр ExplicitCapture. Он отключает все неименованные или неявные захваты в шаблоне регулярных выражений. При использовании этого параметра можно записать только подстроки, соответствующие именованным группам, определенным с элементом (?<name>subexpression) языка. Флаг ExplicitCapture можно передать options параметру Regex конструктора класса или options параметру Regex статического метода сопоставления.

  • Используйте параметр n в языковом элементе (?imnsx). Этот параметр отключает все неименованные или неявные захваты, начиная с места в шаблоне регулярного выражения, где появляется элемент. Захваты отключаются либо до конца шаблона, либо до тех пор, пока параметр (-n) не включает неименованные или неявные захваты. Дополнительные сведения см. в разделе "Прочие конструкции".

  • Используйте параметр n в языковом элементе (?imnsx:subexpression). Этот параметр отключает все неименованные или неявные захваты в subexpression. Записи любых неименованных или неявных вложенных групп записи также отключены.

Потокобезопасность

Сам Regex класс является потокобезопасным и неизменяемым (только для чтения). То есть Regex объекты можно создавать в любом потоке и совместно использовать их между потоками. Методы сопоставления можно вызывать из любого потока и никогда не изменять глобальное состояние.

Однако возвращаемые Match результатом объекты (MatchCollectionиRegex) должны использоваться в одном потоке. Хотя многие из этих объектов логически неизменяемы, их реализации могут отложить вычисление некоторых результатов для повышения производительности, и в результате вызывающие стороны должны сериализовать доступ к ним.

Если у вас есть необходимость совместно использовать Regex объекты результатов в нескольких потоках, эти объекты можно преобразовать в потокобезопасные экземпляры, вызвав их синхронизированные методы. За исключением перечислителей, все классы регулярных выражений являются потокобезопасными или могут быть преобразованы в потокобезопасные объекты синхронизированным методом.

Перечислители являются единственным исключением. Необходимо сериализовать вызовы перечислителей коллекции. Правило заключается в том, что если коллекция может быть перечислена на нескольких потоках одновременно, необходимо синхронизировать методы перечислителя в корневом объекте коллекции, пересекаемой перечислителем.

Название Описание
Подробная информация о поведении регулярных выражений Проверяет реализацию обработчика регулярных выражений в .NET. В этой статье рассматривается гибкость регулярных выражений и объясняется ответственность разработчика за обеспечение эффективной и надежной работы подсистемы регулярных выражений.
Поиск с возвратом Объясняет, что такое бэктрекинг и как он влияет на производительность регулярных выражений, и рассматривает элементы языка, которые предоставляют альтернативы бэктрекингу.
Язык регулярных выражений — краткий справочник Описывает элементы языка регулярного выражения в .NET и содержит ссылки на подробную документацию по каждому элементу языка.