مقدمة: أسطورة سرعة التعبيرات المنتظمة
\n\nإذا كنت قد كتبت مرة واحدة على الأقل بلغة C# واستخدمت التعبيرات المنتظمة، فمن المؤكد أنك واجهت معضلة: استخدام \\d أم [0-9] للبحث عن الأرقام؟ للوهلة الأولى لا يوجد فرق - كلاهما يجد الأرقام. لكن المطورين ذوي الخبرة، الذين يصقلون الأداء إلى أقصى حد، يعرفون: \\d يمكن أن يكون أبطأ بشكل ملحوظ من [0-9].
على Stack Overflow، جمع هذا السؤال آلاف المشاهدات وعشرات التعليقات. اليوم لن نعيد سرد الإجابة فحسب، بل سننظر تحت غطاء محرك .NET Regex ونفهم لماذا يحدث هذا و كيف يؤثر على الكود الخاص بك. سنكتب معايير حقيقية بلغة C#، وننظر إلى كود IL، ونعرف متى يجب دق ناقوس الخطر ومتى يمكننا الاسترخاء.
\n\nهل أنت مستعد؟ هيا بنا!
\n\nما هما \\d و [0-9] في C#؟
\n\nفي البداية - الأساسيات. في معظم تطبيقات التعبيرات المنتظمة (بما في ذلك C#) \\d هو اختصار لـ "أي رقم". و [0-9] هو فئة أحرف تعدد صراحة النطاق من '0' إلى '9'.
يبدو أنهما نفس الشيء. لكن الشيطان، كما هو الحال دائمًا، في التفاصيل.
\n\nUnicode مقابل ASCII
\n\nفي .NET (بدءًا من .NET Framework 4.5 فصاعدًا) \\d بشكل افتراضي يطابق أي حرف من فئة Unicode Nd (Number, Decimal Digit). هذه ليست فقط الأرقام العربية 0-9، بل تشمل أيضًا:
- \n
- أرقام الديفاناغاري (०१२३...) \n
- أرقام الكتابة التاميلية \n
- الأرقام العربية الشرقية \n
- وعشرات الأنظمة العددية الأخرى \n
أما [0-9] فهو نطاق محدد بشكل صارم من رموز ASCII (48-57). فقط 0،1،2،3،4،5،6،7،8،9. لا يوجد Unicode.
لماذا السرعة مختلفة؟ تحليل تحت الغطاء
\n\nالآن الشيء الرئيسي: لماذا \\d أبطأ؟ دعنا نفهم ذلك على مستوى المحرك.
1. عدد الفحوصات
\n\nعندما يواجه المحرك [0-9]، فإنه يقوم بشيء واحد: يتحقق مما إذا كان رمز الحرف يقع في النطاق 48-57. هذه مقارنة واحدة (أو اثنتان، إذا أخذنا في الاعتبار الحدود).
عندما يواجه المحرك \\d، يجب عليه:
- \n
- الحصول على فئة Unicode للحرف (هذا أغلى من مجرد int). \n
- مقارنتها مع الفئة
Nd. \n
فئة Unicode ليست مجرد رقم، بل هي علامة كاملة يتم حسابها من خلال جداول داخلية. بالنسبة للأرقام اللاتينية، ستكون النتيجة متطابقة، لكن الطريق إليها أطول.
\n\n2. التخزين المؤقت و JIT
\n\nفي .NET، يتم تجميع Regex إلى كود وسيط (IL) أو حتى إلى كود أصلي (إذا تم استخدام RegexOptions.Compiled). بالنسبة لـ [0-9]، يمكن لـ JIT توليد تسلسل بسيط جدًا من التعليمات: زوج من المقارنات ونقل شرطي. بالنسبة لـ \\d - استدعاء دالة CharUnicodeInfo.GetUnicodeCategory ثم المقارنة.
استدعاء الدالة له تكاليف إضافية: دفع الوسائط، call، فحص المكدس. حتى لو كانت الدالة صغيرة، فهي لا تزال أبطأ من المقارنة المضمنة.
\n\n3. مثال من معيار حقيقي
\n\nدعنا نكتب اختبارًا بسيطًا بلغة C# باستخدام BenchmarkDotNet (تأكد من تثبيته عبر NuGet إذا كنت ستكرره):
\n\nusing BenchmarkDotNet.Attributes;\nusing BenchmarkDotNet.Running;\nusing System.Text.RegularExpressions;\n\npublic class RegexBenchmark\n{\n private static readonly Regex _digitPattern = new Regex(@"\\d", RegexOptions.Compiled);\n private static readonly Regex _rangePattern = new Regex(@"[0-9]", RegexOptions.Compiled);\n \n private const string TestString = "Hello123World456!";\n\n [Benchmark]\n public bool DigitPattern() => _digitPattern.IsMatch(TestString);\n\n [Benchmark]\n public bool RangePattern() => _rangePattern.IsMatch(TestString);\n}\n\nclass Program\n{\n static void Main() => BenchmarkRunner.Run<RegexBenchmark>();\n}\n\nالنتائج (على .NET