Почему \d в C# работает медленнее, чем [0-9]?
Ключевое отличие: Unicode vs ASCII
Вы столкнулись с классической ловушкой, которая сбивает с толку многих разработчиков. В C# (и в .NET в целом) класс \d по умолчанию не эквивалентен [0-9]. Он соответствует любой цифре Unicode, а не только ASCII-цифрам от 0 до 9.
Давайте разберемся подробнее. В спецификации Unicode есть множество символов, которые считаются цифрами, помимо привычных 0-9. Например:
- Арабско-индийские цифры: ٠١٢٣٤٥٦٧٨٩
- Тибетские цифры: ༠༡༢༣༤༥༦༧༨༩
- Цифры в кружочках: ①②③④⑤⑥⑦⑧⑨
- И многие другие (всего около 650 символов в категории Nd — Number, Decimal Digit)
Когда вы используете \d, движок регулярных выражений должен для каждого символа строки:
- Проверить, принадлежит ли символ категории Unicode "Nd" (Decimal Digit Number).
- Для этого нужно обратиться к таблицам Unicode, что значительно медленнее, чем простое сравнение с диапазоном ASCII.
Вот пример кода, который наглядно демонстрирует разницу:
using System;
using System.Text.RegularExpressions;
using System.Diagnostics;
class Program
{
static void Main()
{
// Строка с арабскими цифрами (не ASCII!)
string testString = "٠١٢٣٤٥٦٧٨٩";
Console.WriteLine("Проверка строки с Unicode-цифрами:");
Console.WriteLine($"\\d: {Regex.IsMatch(testString, @"^\d+$")}"); // True
Console.WriteLine($"[0-9]: {Regex.IsMatch(testString, @"^[0-9]+$")}"); // False
// Бенчмарк на ASCII-цифрах
string asciiDigits = new string('9', 10000);
var sw = Stopwatch.StartNew();
for (int i = 0; i < 1000; i++)
{
Regex.IsMatch(asciiDigits, @"^\d+$");
}
sw.Stop();
Console.WriteLine($"\n\\d на ASCII: {sw.ElapsedMilliseconds} ms");
sw.Restart();
for (int i = 0; i < 1000; i++)
{
Regex.IsMatch(asciiDigits, @"^[0-9]+$");
}
sw.Stop();
Console.WriteLine($"[0-9] на ASCII: {sw.ElapsedMilliseconds} ms");
}
}
Как исправить ситуацию?
У вас есть три варианта, в зависимости от ваших потребностей:
- Используйте
[0-9]— самый простой и быстрый способ, если вам нужны только ASCII-цифры. - Используйте флаг
RegexOptions.ECMAScript— он заставляет\dработать как в JavaScript (только ASCII 0-9):
var regex = new Regex(@"^\d+$", RegexOptions.ECMAScript);
bool result = regex.IsMatch("12345"); // Быстро и только ASCII
- Используйте
[0-9]с компиляцией — для максимальной производительности в циклах:
var regex = new Regex(@"^[0-9]+$", RegexOptions.Compiled);
Почему это важно?
Разница в производительности становится критичной при обработке больших объемов данных. Например, при парсинге логов, валидации форм или работе с CSV-файлами. В таких сценариях выбор между \d и [0-9] может дать прирост скорости в 3-5 раз.
Запомните простое правило: если вам нужны только "обычные" цифры 0-9 — всегда используйте [0-9] или флаг ECMAScript. Это сделает ваш код не только быстрее, но и предсказуемее.
Дополнительный совет: используйте RegexOptions.Compiled
Коллега выше отлично объяснил разницу между Unicode и ASCII. Я хочу добавить еще один важный момент — компиляцию регулярных выражений.
Если вы используете одну и ту же регулярку многократно (например, в цикле), обязательно создавайте объект Regex один раз и используйте флаг RegexOptions.Compiled. Это заставит .NET скомпилировать регулярное выражение в MSIL-код, что даст значительный прирост производительности.
Плохой пример (регулярка создается каждый раз):
for (int i = 0; i < 10000; i++)
{
if (Regex.IsMatch(data[i], @"^[0-9]+$"))
{
// Обработка
}
}
Хороший пример:
private static readonly Regex _digitRegex = new Regex(@"^[0-9]+$", RegexOptions.Compiled);
for (int i = 0; i < 10000; i++)
{
if (_digitRegex.IsMatch(data[i]))
{
// Обработка
}
}
Комбинация [0-9] + RegexOptions.Compiled + static readonly даст вам максимальную производительность. В некоторых бенчмарках такой подход оказывается быстрее нескомпилированной версии в 10-20 раз, особенно на коротких строках.
Также стоит помнить, что если вам нужно просто проверить, состоит ли строка из цифр, без регулярных выражений можно обойтись методом string.All(char.IsDigit) — он тоже учитывает Unicode, но работает быстрее, чем \d в регулярке.