Avatar ·

Почему \d в C# работает медленнее, чем [0-9]?

Всем привет! Я начинающий C# разработчик и столкнулся с неожиданной проблемой при оптимизации регулярных выражений. У меня есть задача: проверять, состоит ли строка только из цифр. Я написал два варианта регулярки: 1. `^\d+$` 2. `^[0-9]+$` Интуитивно кажется, что `\d` должен быть быстрее или хотя бы таким же по скорости, ведь это просто сокращение для цифр. Но я провел небольшой бенчмарк с помощью `Stopwatch` на строке длиной 1000 символов (1000 случайных цифр) и получил шокирующие результаты: ``` \d: ~450 тиков [0-9]: ~120 тиков ``` Разница почти в 4 раза! Причем я прогнал тест 100 раз, чтобы исключить случайность. Результаты стабильны. Я использую .NET 6, стандартный `Regex` без флагов. Почему так происходит? Ведь и `\d`, и `[0-9]` должны обозначать одно и то же — цифры от 0 до 9. Может быть, я что-то неправильно измеряю? Или в C# есть какие-то подводные камни с Unicode? Подскажите, пожалуйста, в чем дело и как правильно писать регулярки для поиска цифр, чтобы они работали максимально быстро. Заранее спасибо! <<>>

Ключевое отличие: Unicode vs ASCII

Вы столкнулись с классической ловушкой, которая сбивает с толку многих разработчиков. В C# (и в .NET в целом) класс \d по умолчанию не эквивалентен [0-9]. Он соответствует любой цифре Unicode, а не только ASCII-цифрам от 0 до 9.

Давайте разберемся подробнее. В спецификации Unicode есть множество символов, которые считаются цифрами, помимо привычных 0-9. Например:

  • Арабско-индийские цифры: ٠١٢٣٤٥٦٧٨٩
  • Тибетские цифры: ༠༡༢༣༤༥༦༧༨༩
  • Цифры в кружочках: ①②③④⑤⑥⑦⑧⑨
  • И многие другие (всего около 650 символов в категории Nd — Number, Decimal Digit)

Когда вы используете \d, движок регулярных выражений должен для каждого символа строки:

  1. Проверить, принадлежит ли символ категории Unicode "Nd" (Decimal Digit Number).
  2. Для этого нужно обратиться к таблицам 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");
    }
}

Как исправить ситуацию?

У вас есть три варианта, в зависимости от ваших потребностей:

  1. Используйте [0-9] — самый простой и быстрый способ, если вам нужны только ASCII-цифры.
  2. Используйте флаг RegexOptions.ECMAScript — он заставляет \d работать как в JavaScript (только ASCII 0-9):
var regex = new Regex(@"^\d+$", RegexOptions.ECMAScript);
bool result = regex.IsMatch("12345"); // Быстро и только ASCII
  1. Используйте [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 в регулярке.