تتيح الوراثة (inheritance) لصنف أن يبني على آخر. يحصل الصنف المشتق على أعضاء الصنف الأساس، ويضيف أعضاءه، وقد يستبدل السلوك الذي يسمح له الصنف الأساس باستبداله. ومع الدوال virtual تمنحك تعدد الأشكال (polymorphism): الشيفرة المكتوبة مقابل الصنف الأساس تشغّل السلوك المشتق الصحيح دون أن تعرف أي صنف مشتق لديها.
اشتقاق صنف
ضع الصنف الأساس بعد نقطتين. للصنف المشتق كل ما للصنف الأساس، إضافة إلى ما يصرّح به:
المخرجات:
TX-19: 20 km, 2 fares
True
ما يحصل عليه الصنف المشتق وما لا يحصل عليه:
- يُورث: الحقول والخصائص والدوال والأحداث والأنواع المتداخلة. كل شيء موجود فعليًا في الكائن.
- يمكن الوصول إليه: فقط ما يسمح به الأساس: الأعضاء
publicوprotectedوinternal. العضوprivateفيVehicleموجود داخل كلTaxiلكن شيفرةTaxiلا تستطيع تسميته. ولهذا لـKmضابطprivate set: يستطيعTaxiقراءتها، ولا يغيّرها إلا عبرDrive. - لا يُورث: المُنشئات. يجب أن يصرّح
Taxiبمُنشئاته ويتسلسل إلى أحد مُنشئاتVehicleبـ: base(plate). تُظهر صفحة المُنشئات ترتيب تنفيذ الاثنين.
كل صنف مشتق في النهاية من object، ولهذا لكل كائن ToString() وEquals() وGetHashCode().
virtual وoverride
يعلّم الصنف الأساس دالة بـ virtual ليقول "يمكن للأصناف المشتقة تقديم نسختها الخاصة". ويستبدلها الصنف المشتق بـ override. وداخل التجاوز تستدعي base.Method() نسخة الصنف الأساس.
المخرجات:
[email] to lea@example.com: Your order 1042 has shipped today
(unsubscribe link appended)
[sms] to +351 912 000 111: Your order 1042 has...
[generic] to ops-team: Your order 1042 has shipped today
متغيّر الحلقة من نوع Notification، ومع ذلك يعرض كل كائن نفسه بطريقته. هذا هو تعدد الأشكال: يُحسم الاستدعاء n.Render(...) وقت التشغيل من النوع الفعلي للكائن. لاحظ أيضًا أن base.Render في Email تستخدم Channel، التي هي نفسها افتراضية، فتطبع دالة الأساس email لا generic. الاستدعاء الافتراضي داخل الصنف الأساس ما زال يصل إلى التجاوز.
يمكن للخصائص أن تكون افتراضية أيضًا، كما تُظهر Channel. أما الحقول فلا.
يلزمك المترجم بالكلمتين كلتيهما. كتابة override على دالة ليست virtual هي الخطأ CS0506 ("cannot override inherited member ... because it is not marked virtual, abstract, or override"). أما ترك override حين تكون دالة الأساس افتراضية فمجرد تحذير، ويغيّر المعنى كليًا، كما يُظهر القسم التالي.
new مقابل override: الإخفاء بدل التجاوز
إن صرّح صنف مشتق بدالة لها توقيع دالة أساس دون كتابة override، فإنه يخفي دالة الأساس. يحذّر المترجم (CS0114 لدالة أساس افتراضية، وCS0108 في غير ذلك) ويقترح الكلمة new، التي تسكت التحذير لكنها تبقي سلوك الإخفاء:
المخرجات:
Sales report
Report
Draft report
b وc من نوع الكائن نفسه، ومع ذلك يطبعان عنوانين مختلفين. مع new تعتمد الدالة المختارة على نوع المتغيّر، ويُحسم ذلك وقت الترجمة. الشيفرة التي تتعامل مع التقارير على أنها Report (قائمة، أو معامل دالة، أو استدعاء راجع من إطار) لا ترى DraftReport.Title أبدًا. ونادرًا جدًا ما يكون هذا ما تريده. استخدم override لتعدد الأشكال؛ وnew موجودة أساسًا للحالة التي يضيف فيها صنف أساس لا تتحكم فيه عضوًا يتصادم اسمه مع عضو لديك أصلًا.
sealed
تمنع sealed على صنف الاشتقاق منه:
sealed class Invoice { }
class CorrectedInvoice : Invoice { }
// error CS0509: 'CorrectedInvoice': cannot derive from sealed type 'Invoice'
string مختوم، وكذلك أنواع كثيرة في الإطار. وعلى تجاوز توقف sealed السلسلة عند ذلك المستوى:
class Shape { public virtual string Name() => "shape"; }
class Square : Shape { public sealed override string Name() => "square"; }
class Tile : Square { public override string Name() => "tile"; }
// error CS0239: 'Tile.Name()': cannot override inherited member 'Square.Name()' because it is sealed
تصميم صنف للوراثة يتطلب عملًا: تقرير ما هو افتراضي، وما يمكن للأصناف المشتقة الاعتماد عليه، وبأي ترتيب تحدث الأشياء. الصنف الذي لم يُصمَّم كذلك أكثر أمانًا مختومًا، ويمكن التراجع عن الختم لاحقًا دون كسر أحد، بينما لا يمكن التراجع عن فك الختم بعد أن يشتق منك آخرون. وقد تكون استدعاءات أعضاء الأصناف المختومة أسرع قليلًا أيضًا، لأن وقت التشغيل يعرف أنه لا يوجد تجاوز.
صنف أساس واحد، وواجهات كثيرة
لصنف C# صنف أساس واحد بالضبط. class Admin : User, Employee هو الخطأ CS1721 ("cannot have multiple base classes"). لكن يمكن للصنف تنفيذ أي عدد من الواجهات إلى جانب صنفه الأساس:
class Admin : User, IAuditable, IComparable<Admin>
{
// base class first, then interfaces, in any order
}
استخدم صنفًا أساسًا لـ "نوع من، ويتشارك التنفيذ مع"، والواجهات لـ "يستطيع أن". وحين لا يوجد الصنف الأساس إلا لإجبار الأصناف المشتقة على ملء بعض الدوال، فالصنف المجرّد هو الأداة.
التحويل صعودًا ونزولًا في التسلسل الهرمي
يمكن دائمًا استخدام كائن مشتق حيث يُتوقع نوعه الأساس. هذا التحويل صعودًا (upcast) ضمني ولا يمكن أن يفشل. أما الاتجاه الآخر، التحويل نزولًا (downcast)، فيحتاج إلى تحويل صريح ويفشل وقت التشغيل إن لم يكن الكائن من ذلك النوع:
المخرجات:
Rex fetches the ball
InvalidCastException: Tom is not a Dog
True
Rex fetches the ball
كتابة Dog d = pet; دون التحويل خطأ ترجمة (CS0266: "An explicit conversion exists (are you missing a cast?)")، لأن المترجم لا يعرف إلا أن pet من نوع Animal ما. فضّل is مع متغيّر حين قد يكون الكائن من نوع آخر، والتحويل الصريح العادي فقط حين يكون أي شيء آخر خطأً برمجيًا. التحويل نزولًا المتكرر علامة على تصميم سيئ: يعني عادة أن السلوك مكانه دالة افتراضية في الصنف الأساس. تغطي صفحة مطابقة الأنماط أشكال is كاملة.
الوراثة والمجموعات
يعمل تعدد الأشكال عنصرًا عنصرًا، لكن المجموعات العامة من نوع مشتق ليست مجموعات من النوع الأساس: لا تُترجم List<Animal> animals = new List<Dog>();، لأن القائمة ستقبل عندها Cat. أما العروض للقراءة فقط فمتغايرة (covariant)، فتكون IEnumerable<Animal> animals = new List<Dog>(); مقبولة. تشرح صفحة الأنواع العامة السبب.
أخطاء شائعة
- نسيان
override. تُترجم الدالة مع تحذير وتخفي بصمت بدل أن تتجاوز. عامل CS0114 كخطأ. - جعل كل شيء
virtual. كل عضو افتراضي وعد للأصناف المشتقة بموعد استدعائه وما يمكنه افتراضه. لا تعلّم إلا نقاط التوسيع التي تقصدها. - التسلسلات الهرمية العميقة. ثلاثة أو أربعة مستويات وراثة تجعل معرفة أي نسخة من الدالة تُنفَّذ صعبة. التركيب، أي صنف يحمل كائنًا آخر ويستدعيه، أبسط غالبًا من مستوى جديد.
- استدعاء دوال افتراضية من مُنشئ. يُنفَّذ التجاوز قبل جسم المُنشئ المشتق، فيظل أي حقل يُسنده ذلك الجسم يحمل قيمته الافتراضية.
- استخدام الوراثة لإعادة استخدام الشيفرة فقط. إن ورث
StackمنList، يستطيع المستدعون استخدامInsertفي منتصف مكدّسك. احملListفي حقل خاص بدلًا من ذلك.
الأسئلة الشائعة
كيف تعمل الوراثة في C#؟
يسمّي الصنف صنفًا أساسًا واحدًا بعد نقطتين: class Dog : Animal. يحصل الصنف المشتق على كل أعضاء الصنف الأساس (الحقول والخصائص والدوال والأحداث)، ويمكنه إضافة أعضائه، ويمكنه تجاوز ما علّمه الأساس بـ virtual. المُنشئات لا تُورث، والأعضاء private، مع أنها موجودة في الكائن، لا يمكن الوصول إليها من الصنف المشتق.
ما الفرق بين virtual وoverride في C#؟
توضع virtual على دالة الصنف الأساس وتقول "يمكن للأصناف المشتقة استبدال هذه". وتوضع override على دالة الصنف المشتق وتنفّذ الاستبدال. كلتاهما لازمة: تجاوز دالة ليست virtual أو abstract أو override أصلًا هو الخطأ CS0506. وحين تُستدعى دالة افتراضية يشغّل وقت التشغيل نسخة النوع الفعلي للكائن، لا نوع المتغيّر.
ما الفرق بين new وoverride في C#؟
تستبدل override دالة الأساس لكل مستدعٍ، حتى الشيفرة التي تمسك الكائن عبر متغيّر من نوع الصنف الأساس. أما new فتخفيها فقط: الشيفرة التي ترى الكائن كنوع مشتق تستدعي الدالة الجديدة، بينما الشيفرة التي تراه كنوع أساس ما زالت تستدعي دالة الأساس. فتشغّل Base b = new Derived(); b.M(); الدالة Derived.M مع override وBase.M مع new.
هل تدعم C# الوراثة المتعددة؟
ليس للأصناف: للصنف صنف أساس واحد بالضبط، وسرد اثنين هو الخطأ CS1721. لكن يمكن للصنف تنفيذ أي عدد من الواجهات، وهكذا تنمذج C# فكرة "هذا النوع يستطيع فعل عدة أشياء". ومنذ C# 8 يمكن للواجهات أيضًا أن تحمل تنفيذات افتراضية للدوال.
ماذا تعني sealed في C#؟
لا يمكن استخدام الصنف sealed صنفًا أساسًا؛ الاشتقاق منه هو الخطأ CS0509. string مختوم مثلًا. وعلى دالة تمنع sealed override الأصناف الأدنى في التسلسل الهرمي من تجاوزها مجددًا (CS0239). ختم الأصناف التي لم تُصمَّم للوراثة افتراضي معقول.
كيف أستدعي دالة الصنف الأساس في C#؟
استخدم base.MethodName(...) داخل الصنف المشتق، عادة داخل التجاوز: public override string Describe() => base.Describe() + " with GPS";. وللمُنشئات استخدم : base(...) بعد قائمة المعاملات لاختيار أي مُنشئ أساس يُنفَّذ.