طريقة عامة مع تقييد T على Enum: تحت الغطاء في C#

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

طريقة عامة مع تقييد T على Enum: كيف تعمل تحت الغطاء

\n\n

تخيل: أنت تكتب بلغة C# وتريد إنشاء طريقة عامة تقبل أي تعداد (Enum). يبدو الأمر بسيطًا؟ ولكن إذا حاولت يومًا كتابة public void DoSomething<T>() where T : Enum، فستعرف أن المترجم يعطي خطأ. قبل C# 7.3 كان هذا مستحيلاً. وبعدها أصبح ممكنًا، ولكن مع تفاصيل دقيقة. دعنا نفهم كيف يعمل هذا تحت الغطاء، ولماذا هو أحد أكثر المواضيع شيوعًا على Stack Overflow.

\n\n

هذه المقالة ليست مجرد إعادة جافة للوثائق. سنتعمق أكثر: من كود IL إلى توصيات ReSharper. هيا بنا!

\n\n

لماذا لم يكن من الممكن ببساطة كتابة where T : Enum؟

\n\n

لفترة طويلة، منعت C# استخدام Enum أو Delegate كتقييد للمعلمات العامة. لماذا؟ لأن المترجم كان بحاجة لضمان أن النوع T هو نوع قيمي (struct) أو نوع مرجعي (class). Enum هو نوع هيكلي خاص يرث من System.Enum. لكن في نظام أنواع C#، لم يكن يتناسب مع القواعد البسيطة:

\n\n
    \n
  • Enum هو struct، ولكن ليس أي struct.
  • \n
  • Enum يرث من ValueType، لكن المترجم لم يسمح باستخدام where T : System.Enum.
  • \n
  • محاولة كتابة where T : struct, Enum لم تنجح أيضًا.
  • \n
\n\n

كان المطورون يلجؤون إلى حلول مؤقتة: التحقق في وقت التشغيل عبر typeof(T).IsEnum أو استخدام الانعكاس. لكن هذا كان يقتل الأداء وسلامة الأنواع.

\n\n

C# 7.3: الحل المنتظر

\n\n

بدءًا من C# 7.3 و .NET Core 2.0 / .NET Framework 4.8، أصبح من الممكن كتابة:

\n\n
public 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.

\n\n

مثال على كود 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.

\n\n

كيف تستخدم الطريقة العامة مع Enum عمليًا؟

\n\n

لنقم بإنشاء مثال مفيد: طريقة لتحليل سلسلة نصية إلى أي تعداد مع دعم تجاهل حالة الأحرف.

\n\n
public 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، وبالتالي فإن التحويل آمن.

\n\n

خيار أكثر أناقة مع Enum.TryParse<T>

\n\n

بدءًا من .NET 5، ظهر تحميل زائد

المدونات

توصيات الكتب