طريقة عامة مع تقييد T على Enum: كيف تعمل تحت الغطاء
\n\nتخيل: أنت تكتب بلغة C# وتريد إنشاء طريقة عامة تقبل أي تعداد (Enum). يبدو الأمر بسيطًا؟ ولكن إذا حاولت يومًا كتابة public void DoSomething<T>() where T : Enum، فستعرف أن المترجم يعطي خطأ. قبل C# 7.3 كان هذا مستحيلاً. وبعدها أصبح ممكنًا، ولكن مع تفاصيل دقيقة. دعنا نفهم كيف يعمل هذا تحت الغطاء، ولماذا هو أحد أكثر المواضيع شيوعًا على Stack Overflow.
هذه المقالة ليست مجرد إعادة جافة للوثائق. سنتعمق أكثر: من كود IL إلى توصيات ReSharper. هيا بنا!
\n\nلماذا لم يكن من الممكن ببساطة كتابة where T : Enum؟
\n\nلفترة طويلة، منعت C# استخدام Enum أو Delegate كتقييد للمعلمات العامة. لماذا؟ لأن المترجم كان بحاجة لضمان أن النوع T هو نوع قيمي (struct) أو نوع مرجعي (class). Enum هو نوع هيكلي خاص يرث من System.Enum. لكن في نظام أنواع C#، لم يكن يتناسب مع القواعد البسيطة:
- \n
- Enum هو struct، ولكن ليس أي struct. \n
- Enum يرث من ValueType، لكن المترجم لم يسمح باستخدام
where T : System.Enum. \n - محاولة كتابة
where T : struct, Enumلم تنجح أيضًا. \n
كان المطورون يلجؤون إلى حلول مؤقتة: التحقق في وقت التشغيل عبر typeof(T).IsEnum أو استخدام الانعكاس. لكن هذا كان يقتل الأداء وسلامة الأنواع.
C# 7.3: الحل المنتظر
\n\nبدءًا من C# 7.3 و .NET Core 2.0 / .NET Framework 4.8، أصبح من الممكن كتابة:
\n\npublic static string GetEnumName<T>(T value) where T : Enum\n{\n return Enum.GetName(typeof(T), value);\n}\n\n\nالآن يفهم المترجم: T هو تعداد. ولكن كيف يفعل ذلك بالضبط؟ دعنا نلقي نظرة تحت الغطاء.
\n\nماذا يحدث في كود IL؟
\n\nعند كتابة where T : Enum، يولد المترجم استدعاء مقيد (constrained call) خاصًا لطرق مثل Enum.HasFlag، Enum.GetName، إلخ. بدلاً من استخدام التغليف (boxing) أو الانعكاس، يمكن لمترجم JIT أن يضع استدعاء مباشر لطريقة ثابتة، مع معرفة النوع المحدد T.
مثال على كود IL للطريقة أعلاه:
\n\n.method public hidebysig static \n string GetEnumName<([mscorlib]System.Enum) T>(!!0 value) cil managed\n{\n // استدعاء Enum.GetName مع بادئة مقيدة\n call string [mscorlib]System.Enum::GetName(class [mscorlib]System.Type, object)\n}\n\n\nلاحظ السمة ([mscorlib]System.Enum) T. هذا هو التقييد الجديد. يراه JIT ويعرف أن T هو enum، وبالتالي يمكن تجنب التغليف عند تمرير القيمة إلى GetName.
كيف تستخدم الطريقة العامة مع Enum عمليًا؟
\n\nلنقم بإنشاء مثال مفيد: طريقة لتحليل سلسلة نصية إلى أي تعداد مع دعم تجاهل حالة الأحرف.
\n\npublic static T ParseEnum<T>(string value, bool ignoreCase = true) where T : Enum\n{\n if (string.IsNullOrWhiteSpace(value))\n throw new ArgumentException("لا يمكن أن تكون القيمة فارغة أو خالية.", nameof(value));\n\n if (!Enum.TryParse(typeof(T), value, ignoreCase, out object? result))\n throw new ArgumentException($"'{value}' ليست قيمة صالحة للتعداد {typeof(T).Name}.");\n\n return (T)result;\n}\n\n\nهام: على الرغم من أن T هو Enum، فإن التحويل (T)result لا يزال يتطلب تحويلاً صريحًا. هذا لأن النوع object في وقت التشغيل لا يعرف المعامل العام. لكن المترجم يضمن أن T هو enum، وبالتالي فإن التحويل آمن.
خيار أكثر أناقة مع Enum.TryParse<T>
\n\nبدءًا من .NET 5، ظهر تحميل زائد