Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Регулярное выражение, или regex, — это строка, которая позволяет разработчику задать шаблон поиска, что является распространённым способом поиска текста и извлечения результатов как подмножества из этой строки. В .NET пространство имен System.Text.RegularExpressions используется для определения экземпляров Regex, статических методов и сопоставления с пользовательскими шаблонами. В этой статье вы узнаете, как использовать генерацию кода, чтобы генерировать экземпляры Regex для оптимизации производительности.
Примечание.
По возможности используйте регулярные выражения, созданные источником, вместо компиляции регулярных выражений с помощью опции RegexOptions.Compiled. Генерация кода может помочь вашему приложению запускаться и работать быстрее, а также быть более оптимизируемым. Чтобы узнать, когда возможно создание источника, см. статью "Когда его использовать".
Скомпилированные регулярные выражения
Когда вы пишете new Regex("somepattern"), происходит несколько вещей. Указанный шаблон анализируется как для обеспечения допустимости шаблона, так и для преобразования его в внутреннее дерево, представляющее синтаксический ретекс. Затем дерево оптимизировано различными способами, преобразуя шаблон в функционально эквивалентный вариант, который может быть более эффективно выполнен. Дерево записывается в форму, которая может быть интерпретирована как ряд опкодов и операндов, предоставляющих инструкции regex-движку интерпретации, как выполнять сопоставление. При выполнении совпадения интерпретатор просто проходит через эти инструкции, обрабатывая их относительно входного текста. При создании нового Regex экземпляра или вызове одного из статических методов Regex интерпретатор используется движком по умолчанию.
При указании RegexOptions.Compiled выполняются все работы, связанные с этапом строительства. Результирующие инструкции преобразуются компилятором, использующим отражение-излучение, в инструкции IL, которые записываются в несколько DynamicMethod объектов. При выполнении сопоставления эти DynamicMethod методы вызываются. Этот промежуточный язык (IL) по сути выполняет то же, что и интерпретатор, но специализируется на точной обработке рассматриваемого шаблона. Например, если шаблон содержит [ac], интерпретатор увидит опкод, который указывает "сравнить входной символ в текущей позиции с набором, указанным в этом описании." Хотя скомпилированный IL будет содержать код, который фактически говорит: "сравните входной символ в текущей позиции с 'a' или 'c'". Специальная обработка случаев и возможность выполнять оптимизации на основе знаний о шаблоне являются основными причинами, по которым указание RegexOptions.Compiled обеспечивает гораздо более быстрое сопоставление, чем интерпретатор.
Существует несколько недостатков RegexOptions.Compiled. Наиболее значительный фактор заключается в том, что строительство обходится дорого. Не только оплачиваются те же затраты, что и для интерпретатора, но затем необходимо скомпилировать полученное RegexNode дерево и сформированные коды операций/операнды в IL, что добавляет дополнительные и нетривиальные расходы. Созданный IL дополнительно необходимо скомпилировать JIT при первом использовании, что приведет к еще большему расходу при запуске.
RegexOptions.Compiled представляет собой фундаментальный компромисс между затратами на первое использование и накладные расходы на каждое последующее использование. Использование System.Reflection.Emit также препятствует использованию RegexOptions.Compiled в определенных средах; некоторые операционные системы не позволяют динамически создавать код, а в таких системах Compiled становится no-op.
Создание источника
В .NET 7 появился новый RegexGenerator генератор источника. Генератор источника — это компонент, подключающийся к компилятору и расширяющий модуль компиляции с дополнительным исходным кодом. Пакет SDK для .NET включает генератор исходного кода, который распознает атрибут GeneratedRegexAttribute в частичном методе, который возвращает Regex. Начиная с .NET 9 атрибут также можно применить к частичным свойствам. Генератор источника предоставляет реализацию этого метода или свойства, содержащего всю логику для объекта Regex. Например, ранее вы могли написать код следующим образом:
private static readonly Regex s_abcOrDefGeneratedRegex =
new(pattern: "abc|def",
options: RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static void EvaluateText(string text)
{
if (s_abcOrDefGeneratedRegex.IsMatch(text))
{
// Take action with matching text
}
}
Чтобы использовать генератор источника, необходимо переписать предыдущий код следующим образом:
[GeneratedRegex("abc|def", RegexOptions.IgnoreCase, "en-US")]
private static partial Regex AbcOrDefGeneratedRegex();
private static void EvaluateText(string text)
{
if (AbcOrDefGeneratedRegex().IsMatch(text))
{
// Take action with matching text
}
}
Начиная с .NET 9, можно также применить GeneratedRegexAttribute к частичному свойству вместо частичного метода. Обеспечивается поддержкой частичных свойств в C# 13. В следующем примере показан эквивалент свойства:
[GeneratedRegex("abc|def", RegexOptions.IgnoreCase, "en-US")]
private static partial Regex AbcOrDefGeneratedRegexProperty { get; }
private static void EvaluateText(string text)
{
if (AbcOrDefGeneratedRegexProperty.IsMatch(text))
{
// Take action with matching text
}
}
Совет
Флаг RegexOptions.Compiled игнорируется генератором источника, поэтому он не нужен в исходной версии.
Созданная реализация AbcOrDefGeneratedRegex() аналогично кэширует одиночный Regex экземпляр, поэтому дополнительное кэширование для использования кода не требуется.
На следующем рисунке показан снимок экрана кэшированного экземпляра, созданного исходным генератором, в подкласс internalRegex, который эмитирует исходный генератор.
Но, как видно, это не просто делает new Regex(...). Скорее, генератор источника создает в виде кода C# пользовательскую Regexпроизводную реализацию с логикой, аналогичной тому, что RegexOptions.Compiled выдает в IL. Вы получаете все преимущества RegexOptions.Compiled пропускной способности (фактически больше) и преимущества Regex.CompileToAssembly запуска, но без сложности CompileToAssembly. Источник, который выдается, является частью вашего проекта, то есть доступен для просмотра и отладки.
Совет
В Visual Studio щелкните правой кнопкой мыши частичный метод или объявление свойства и выберите "Перейти к определению". Кроме того, выберите узел проекта в Обозреватель решений, затем разверните Зависимости>Анализаторы>System.Text.RegularExpressions.Generator>System.Text.RegularExpressions.Generator.RegexGenerator>RegexGenerator.g.cs, чтобы просмотреть сгенерированный код C# из этого генератора регулярных выражений.
Вы можете задать точки останова в нем, вы можете пошагово выполнить его, и вы можете использовать его в качестве средства обучения, чтобы точно понять, как движок регулярных выражений обрабатывает ваш шаблон с вашими входными данными. Генератор даже создает комментарии с тройной косой чертой (XML), чтобы сделать выражение понятным на первый взгляд и там, где оно используется.
Внутри файлов, сгенерированных из исходного кода
При использовании .NET 7 исходный генератор и RegexCompiler почти полностью перезаписали, принципиально изменяя структуру созданного кода. Этот подход был усовершенствован для обработки всех конструкций (с одной оговоркой), и в соответствии с новым подходом как RegexCompiler, так и генератор исходного кода по-прежнему в основном сопоставляются 1:1 друг с другом. Рассмотрим выходные данные генератора кода для одной из ключевых функций из abc|def выражения.
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
int pos = base.runtextpos;
int matchStart = pos;
ReadOnlySpan<char> slice = inputSpan.Slice(pos);
// Match with 2 alternative expressions, atomically.
{
if (slice.IsEmpty)
{
return false; // The input didn't match.
}
switch (slice[0])
{
case 'A' or 'a':
if ((uint)slice.Length < 3 ||
!slice.Slice(1).StartsWith("bc", StringComparison.OrdinalIgnoreCase)) // Match the string "bc" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 3;
slice = inputSpan.Slice(pos);
break;
case 'D' or 'd':
if ((uint)slice.Length < 3 ||
!slice.Slice(1).StartsWith("ef", StringComparison.OrdinalIgnoreCase)) // Match the string "ef" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 3;
slice = inputSpan.Slice(pos);
break;
default:
return false; // The input didn't match.
}
}
// The input matched.
base.runtextpos = pos;
base.Capture(0, matchStart, pos);
return true;
}
Цель исходного кода заключается в том, чтобы быть понятным, с простой структурой, с комментариями, объясняющими, что делается на каждом шаге, и в целом с кодом, созданным в соответствии с руководящим принципом, что генератор должен выдавать код, как будто человек написал его. Даже при использовании бэктрекинга, структура бэктрекинга становится частью структуры кода, а не делается на основе стека, чтобы указать, куда перейти дальше. Например, приведён ниже код для той же созданной функции сопоставления, когда выражение [ab]*[bc]:
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
int pos = base.runtextpos;
int matchStart = pos;
int charloop_starting_pos = 0, charloop_ending_pos = 0;
ReadOnlySpan<char> slice = inputSpan.Slice(pos);
// Match a character in the set [ABab] greedily any number of times.
//{
charloop_starting_pos = pos;
int iteration = slice.IndexOfAnyExcept(Utilities.s_ascii_600000006000000);
if (iteration < 0)
{
iteration = slice.Length;
}
slice = slice.Slice(iteration);
pos += iteration;
charloop_ending_pos = pos;
goto CharLoopEnd;
CharLoopBacktrack:
if (Utilities.s_hasTimeout)
{
base.CheckTimeout();
}
if (charloop_starting_pos >= charloop_ending_pos ||
(charloop_ending_pos = inputSpan.Slice(charloop_starting_pos, charloop_ending_pos - charloop_starting_pos).LastIndexOfAny(Utilities.s_ascii_C0000000C000000)) < 0)
{
return false; // The input didn't match.
}
charloop_ending_pos += charloop_starting_pos;
pos = charloop_ending_pos;
slice = inputSpan.Slice(pos);
CharLoopEnd:
//}
// Advance the next matching position.
if (base.runtextpos < pos)
{
base.runtextpos = pos;
}
// Match a character in the set [BCbc].
if (slice.IsEmpty || ((uint)((slice[0] | 0x20) - 'b') > (uint)('c' - 'b')))
{
goto CharLoopBacktrack;
}
// The input matched.
pos++;
base.runtextpos = pos;
base.Capture(0, matchStart, pos);
return true;
}
Структуру бэктрекинга в коде можно увидеть с помощью метки CharLoopBacktrack, которая указывает, куда вернуться, и goto, чтобы перейти в это место, если дальнейшая часть регулярного выражения не срабатывает.
Если вы посмотрите на код, реализующий RegexCompiler и генератор исходного кода, они будут выглядеть очень похоже: аналогичные названия методов, аналогичная структура вызовов и даже аналогичные комментарии в реализации. В большинстве случаев они приводят к тому же коду, хотя один в IL и один в C#. Конечно, компилятор C# отвечает за преобразование C# в IL, поэтому результирующий IL в обоих случаях, скорее всего, не будет идентичным. Генератор кода в различных случаях полагается на то, что компилятор C# дополнительно оптимизирует различные конструкции C#, используя этот факт. Имеется несколько конкретных аспектов, в которых генератор источника создаст более оптимизированный код сопоставления, чем делает RegexCompiler. Например, в одном из предыдущих примеров можно увидеть генератор кода, создающий оператор switch, с одной ветвью для 'a' и другой ветвью для 'b'. Поскольку компилятор C# эффективно оптимизирует инструкции switch, имея в своем распоряжении несколько стратегий для этого, генератор кода имеет специальную оптимизацию, которой RegexCompiler не обладает. Для альтернатив исходный генератор рассматривает все ветви, и если может доказать, что каждая ветвь начинается с другого начального символа, он будет использовать оператор switch по этому первому символу и не выводить никакой код отката для этой альтернативы.
Вот немного более сложный пример этого. Альтернации более тщательно анализируются, чтобы определить, можно ли их рефакторинговать таким образом, чтобы они могли быть легче оптимизированы подсистемами обратного отслеживания и приводили к более простому исходному генерированному коду. Одна из таких оптимизаций поддерживает извлечение общих префиксов из ветвей. Если чередование является атомарным, так что упорядочивание не имеет значения, то ветви можно переупорядочить для большего количества таких извлечений. Вы можете увидеть влияние этого на следующий шаблон дня недели Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday, который создает соответствующую функцию, как показано ниже.
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
int pos = base.runtextpos;
int matchStart = pos;
char ch;
ReadOnlySpan<char> slice = inputSpan.Slice(pos);
// Match with 6 alternative expressions, atomically.
{
int alternation_starting_pos = pos;
// Branch 0
{
if ((uint)slice.Length < 6 ||
!slice.StartsWith("monday", StringComparison.OrdinalIgnoreCase)) // Match the string "monday" (ordinal case-insensitive)
{
goto AlternationBranch;
}
pos += 6;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 1
{
if ((uint)slice.Length < 7 ||
!slice.StartsWith("tuesday", StringComparison.OrdinalIgnoreCase)) // Match the string "tuesday" (ordinal case-insensitive)
{
goto AlternationBranch1;
}
pos += 7;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch1:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 2
{
if ((uint)slice.Length < 9 ||
!slice.StartsWith("wednesday", StringComparison.OrdinalIgnoreCase)) // Match the string "wednesday" (ordinal case-insensitive)
{
goto AlternationBranch2;
}
pos += 9;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch2:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 3
{
if ((uint)slice.Length < 8 ||
!slice.StartsWith("thursday", StringComparison.OrdinalIgnoreCase)) // Match the string "thursday" (ordinal case-insensitive)
{
goto AlternationBranch3;
}
pos += 8;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch3:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 4
{
if ((uint)slice.Length < 6 ||
!slice.StartsWith("fr", StringComparison.OrdinalIgnoreCase) || // Match the string "fr" (ordinal case-insensitive)
((((ch = slice[2]) | 0x20) != 'i') & (ch != 'İ')) || // Match a character in the set [Ii\u0130].
!slice.Slice(3).StartsWith("day", StringComparison.OrdinalIgnoreCase)) // Match the string "day" (ordinal case-insensitive)
{
goto AlternationBranch4;
}
pos += 6;
slice = inputSpan.Slice(pos);
goto AlternationMatch;
AlternationBranch4:
pos = alternation_starting_pos;
slice = inputSpan.Slice(pos);
}
// Branch 5
{
// Match a character in the set [Ss].
if (slice.IsEmpty || ((slice[0] | 0x20) != 's'))
{
return false; // The input didn't match.
}
// Match with 2 alternative expressions, atomically.
{
if ((uint)slice.Length < 2)
{
return false; // The input didn't match.
}
switch (slice[1])
{
case 'A' or 'a':
if ((uint)slice.Length < 8 ||
!slice.Slice(2).StartsWith("turday", StringComparison.OrdinalIgnoreCase)) // Match the string "turday" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 8;
slice = inputSpan.Slice(pos);
break;
case 'U' or 'u':
if ((uint)slice.Length < 6 ||
!slice.Slice(2).StartsWith("nday", StringComparison.OrdinalIgnoreCase)) // Match the string "nday" (ordinal case-insensitive)
{
return false; // The input didn't match.
}
pos += 6;
slice = inputSpan.Slice(pos);
break;
default:
return false; // The input didn't match.
}
}
}
AlternationMatch:;
}
// The input matched.
base.runtextpos = pos;
base.Capture(0, matchStart, pos);
return true;
}
В то же время генератор исходного кода сталкивается с другими проблемами, которых просто не существует при непосредственной генерации IL. Если вы посмотрите несколько примеров кода назад, вы можете увидеть некоторые фигурные скобки несколько странно закомментированы. Это не ошибка. Генератор источника признает, что, если эти фигурные скобки не были закомментированы, структура обратного отслеживания зависит от перехода извне области на метку, определенную внутри этой области; такая метка не будет видна таким goto образом, и код не сможет компилироваться. Таким образом, генератор исходного кода должен избежать наличия области на пути. В некоторых случаях это просто закомментирует область видимости, как было сделано здесь. В других случаях, когда это невозможно, иногда может избежать конструкций, требующих областей (таких как блок с несколькими операторами if ), если это будет проблематично.
Генератор исходного кода обрабатывает всё, что и RegexCompiler, кроме одного исключения. Как и при работе с RegexOptions.IgnoreCase, реализации теперь используют таблицу регистра для создания наборов во время инициализации, а сопоставление обратных ссылок также должны обращаться к этой таблице регистра. Эта таблица является внутренней для System.Text.RegularExpressions.dll, и, по крайней мере, на данный момент, код извне этой сборки (в том числе код, генерируемый генератором исходников), не имеет к ней доступа. Это делает обработку IgnoreCase обратных ссылок проблемой в исходном генераторе, и они не поддерживаются. Это единственная конструкция, не поддерживаемая генератором исходного кода, которая поддерживается RegexCompiler. Если вы попытаетесь использовать шаблон, в котором есть один из таких (что бывает редко), генератор исходного кода не будет генерировать пользовательскую реализацию, а вместо этого обратится к кэшированию обычного экземпляра Regex.
Кроме того, ни RegexCompiler, ни генератор исходного кода не поддерживают новый RegexOptions.NonBacktracking. Если указать RegexOptions.Compiled | RegexOptions.NonBacktracking, флаг Compiled просто будет игнорироваться, и если вы указываете NonBacktracking генератору, он будет аналогичным образом переходить на кэширование стандартного экземпляра Regex.
Когда использовать
Общее руководство заключается в том, если вы можете использовать генератор источника, используйте его. Если вы используете Regex в C# с аргументами, известными во время компиляции, и особенно если вы уже используете RegexOptions.Compiled (так как регулярное выражение было определено как критическая точка, которая получит выгоду от повышения эффективности), следует предпочесть использовать генератор исходного кода. Генератор кода предоставит вашему регулярному выражению следующие преимущества:
- Все преимущества пропускной способности
RegexOptions.Compiled. - Преимущества стартапа в том, что не требуется выполнять разбор регулярных выражений, анализ и компиляцию во время выполнения.
- Возможность использования предварительной компиляции с кодом, созданным для регулярного выражения.
- Улучшена отладка и понимание регулярных выражений.
- Возможность уменьшить размер обрезаемого приложения путем обрезки больших полос кода, связанных с
RegexCompiler(и потенциально даже отражение выдает себя).
При использовании с параметром, например RegexOptions.NonBacktracking , для которого генератор источника не может создать пользовательскую реализацию, он по-прежнему выдает кэширование и XML-комментарии, описывающие реализацию, что делает его ценным. Основным недостатком генератора исходного кода является то, что он вставляет дополнительный код в вашу сборку, что может привести к увеличению размера. Чем больше регулярных выражений в вашем приложении и чем они больше, тем больше кода будет генерироваться для них. В некоторых ситуациях, также как и RegexOptions.Compiled может быть ненужным, так и генератор исходного кода. Например, если у вас есть регулярное выражение, которое требуется только редко и для которого пропускная способность не имеет значения, может быть более выгодным просто полагаться на интерпретатор для этого редкого использования.
Внимание
.NET 7 включает в себя анализатор, который определяет использованиеRegex, которое может быть преобразовано в генератор исходного кода, и инструмент исправления, который выполняет преобразование за вас: