استدعاء العضو الظاهري في المنشئ: عندما يحذر مترجم C#، ولا يحذر عبثًا
كل مطور C#، كتب مرة واحدة على الأقل فئات مع الوراثة، لا بد أنه رأى هذا التحذير من ReSharper أو Visual Studio: «استدعاء العضو الظاهري في المنشئ». يبدو غير ضار، لكنه في الواقع علامة حمراء يمكن أن تؤدي إلى أخطاء يصعب اكتشافها وانخفاض في الأداء. اليوم سنشرح ما وراء هذا التحذير، وكيف يرتبط بالأداء، ولماذا حتى المبرمجون ذوو الخبرة يرتكبون هذا الخطأ أحيانًا.
الموضوع ليس من فراغ: سؤال «استدعاء العضو الظاهري في المنشئ» على Stack Overflow حصل على آلاف المشاهدات وعشرات الإجابات. هذا يعني أن المشكلة حقيقية وتؤثر على الكثيرين. لكننا سنذهب أبعد من ذلك: لن نشرح فقط كيفية الإصلاح، بل سنوضح كيفية تحسين الكود، وتجنب التخصيصات الزائدة والسلوك غير المتوقع.
ما هو «استدعاء العضو الظاهري في المنشئ»؟
تخيل موقفًا: تقوم بإنشاء فئة أساسية، وفي منشئها يتم استدعاء طريقة افتراضية. قد يبدو الأمر طبيعيًا، أليس كذلك؟ فالطرق الافتراضية صُممت ليتم تجاوزها. لكن المشكلة هي أنه عند استدعاء طريقة افتراضية من منشئ الفئة الأساسية، فإن كائن الفئة المشتقة لم يتم تهيئته بالكامل بعد. هذا يمكن أن يؤدي إلى استدعاء الطريقة المُعاد تعريفها على كائن «غير مكتمل»، حيث لم يتم تعيين الحقول بعد، ولم يتم حقن التبعيات.
public class BaseClass{ public BaseClass() { // انتبه! استدعاء افتراضي في المنشئ VirtualMethod(); }
public virtual void VirtualMethod() { Console.WriteLine("BaseClass.VirtualMethod"); }}
public class DerivedClass : BaseClass{ private readonly string _data;
public DerivedClass(string data) { _data = data; }
public override void VirtualMethod() { // _data لم تتم تهيئته بعد! Console.WriteLine($"DerivedClass: {_data?.Length ?? 0}"); }}
// الاستخدامvar obj = new DerivedClass("Hello"); // سيطبع: DerivedClass: 0، وليس 5في هذا المثال، ستكون _data مساوية null في وقت استدعاء VirtualMethod، لأن منشئ الفئة المشتقة لم يتم تنفيذه بعد. هذه حالة كلاسيكية حيث يؤدي الاستدعاء الافتراضي في المنشئ إلى سلوك غير واضح.
لماذا هذه مشكلة أداء؟
للوهلة الأولى، هذا مجرد خطأ منطقي. لكن دعنا نتعمق أكثر. عندما تستدعي طريقة افتراضية من المنشئ، تضطر بيئة التشغيل (CLR) إلى إجراء فحوصات إضافية وتوزيع. في الوضع الطبيعي، يكون جدول الطرق الافتراضية (vtable) جاهزًا، ويتم الاستدعاء بسرعة. ومع ذلك، في منشئ الفئة الأساسية، قد لا يكون vtable مبنيًا بالكامل للنوع المشتق. هذا يؤدي إلى:
- تكاليف إضافية للتوزيع — تستهلك CLR وقتًا في التحقق من صحة الاستدعاء.
- احتمال تجميع JIT لرمز غير أمثل — قد لا يتمكن JIT من تضمين (inline) هذا الاستدعاء، مما يقلل الأداء.
- فحوصات إضافية على null — إذا قمت بتجاوز الطريقة، فسيتعين عليك كتابة كود وقائي، مما يزيد من حجم IL ووقت التنفيذ.
في الأنظمة عالية التحميل، حيث يتم استدعاء المنشئات آلاف المرات في الثانية، يمكن أن تتراكم هذه التأخيرات الدقيقة. لذلك، يجب أن يبدأ تحسين كود C# من مثل هذه التفاصيل التي تبدو صغيرة.
متى يمكن أن يكون الاستدعاء الافتراضي في المنشئ مفيدًا؟
إفساد المفاجأة: أبدًا تقريبًا. لكن هناك استثناءات نادرة عندما تعرف بالضبط ما تفعله. على سبيل المثال، في نمط Template Method، حيث تحدد الفئة الأساسية هيكل الخوارزمية، والفئات المشتقة