الصنف المجرّد (abstract class) صنف لا يوجد إلا ليُشتق منه. لا يمكن إنشاء مثيل منه، ويمكنه التصريح بـ أعضاء مجرّدة: دوال وخصائص بلا جسم يجب أن ينفّذها كل صنف مشتق فعلي. وكل ما عدا ذلك فيه صنف عادي: يمكن أن يملك حقولًا ومُنشئات ودوالًا منفّذة كاملًا ترثها الأصناف المشتقة.
التصريح بصنف مجرّد
علّم الصنف بـ abstract، وعلّم كل عضو يجب أن تقدّمه الأصناف المشتقة بـ abstract، منهيًا إياه بفاصلة منقوطة بدل جسم:
المخرجات:
Circle: area 7.07, perimeter 9.42
Rectangle: area 10.00, perimeter 13.00
تُكتب Describe مرة واحدة، في الصنف الأساس، وتستدعي Area وPerimeter، اللتين لا توجدان في Shape إطلاقًا. هذا هو الغرض من العضو المجرّد: يستطيع الصنف الأساس الاعتماد عليه، ويضمن المترجم أن كل شكل فعلي يقدّمه. والصنف المشتق الذي ينسى واحدًا لا يُترجم:
class Triangle : Shape
{
public Triangle() : base("Triangle") { }
public override double Area() => 6;
}
// error CS0534: 'Triangle' does not implement inherited abstract member 'Shape.Perimeter()'
الإصلاح تنفيذ Perimeter، أو تعليم Triangle نفسه بـ abstract إن كان أساسًا وسيطًا لأصناف أخرى.
قواعد الأعضاء المجرّدة
- ليس للعضو المجرّد جسم وهو افتراضي ضمنيًا: تنفّذه الأصناف المشتقة بـ
override. - لا يمكن إلا للأصناف المجرّدة أن تحتوي على أعضاء مجرّدة. ووضع واحد في صنف عادي خطأ ترجمة.
- لا يمكن أن تكون الأعضاء المجرّدة
private(لن يستطيع أحد تجاوزها) ولاstaticفي صنف. - يمكن أن تكون الدوال والخصائص والمفهرسات والأحداث مجرّدة. أما الحقول والمُنشئات فلا.
- لا يلزم أن يحتوي الصنف المجرّد على أي عضو مجرّد. تعليم الصنف
abstractوحده يمنع إنشاء المثيلات فقط، وهذا مفيد لصنف أساس لا معنى له إلا عبر أصنافه الفرعية.
تصرّح الخاصية المجرّدة بالموصّلات التي يجب أن يقدّمها الصنف المشتق:
abstract class Plan
{
public abstract decimal MonthlyPrice { get; }
public abstract string Name { get; }
}
class ProPlan : Plan
{
public override decimal MonthlyPrice => 12.99m;
public override string Name => "Pro";
}
المُنشئات في الأصناف المجرّدة
يمكن أن يملك الصنف المجرّد مُنشئات. لا يمكن استدعاؤها بـ new، لكن كل مُنشئ مشتق يشغّل واحدًا عبر : base(...)، فهي حيث تُهيّأ الحالة المشتركة ويُتحقق منها. صرّح بها protected، وهذا يقول ذلك بالضبط: لا تستطيع استخدام هذا إلا الأصناف المشتقة.
نمط الدالة القالب
أشيع سبب لكتابة صنف مجرّد هو تثبيت ترتيب خوارزمية في الصنف الأساس وترك الأصناف المشتقة تملأ الخطوات منفردة. الدالة الأساس التي تشغّل الخطوات ليست افتراضية، فلا يستطيع أي صنف فرعي إعادة ترتيبها أو تخطيها.
المخرجات:
item,qty,price
Coffee beans,2,11.50
Filter papers,1,3.20
| Item | Qty | Price |
| --- | --- | --- |
| Coffee beans | 2 | 11.50 |
| Filter papers | 1 | 3.20 |
Total: 26.20
كل صف صف مسمّى (tuple)، وتُبنى المخرجات بـ StringBuilder. لاحظ المزج: Header وRow مجرّدتان لأن كل صيغة يجب أن تقرّرهما، وFooter افتراضية بافتراضي فارغ لأن معظم الصيغ لا تحتاج شيئًا هناك. والخطوات protected، فلا تستطيع الشيفرة الخارجية سوى استدعاء Export. وتستخدم أصناف الإطار الفكرة نفسها: Stream وTextWriter أصناف أساس مجرّدة دوالها المريحة الكثيرة مبنية على أعضاء قليلة يقدّمها الصنف المشتق (Read وWrite للتدفق، وWrite(char) لكاتب النص).
الصنف المجرّد مقابل الواجهة
كلاهما يتيح لك كتابة شيفرة مقابل نوع دون معرفة الصنف الفعلي. ويختلفان فيما يمكن أن يحتوياه وفي عدد ما يمكن للصنف امتلاكه منهما:
| الصنف المجرّد | الواجهة | |
|---|---|---|
| كم يمكن للصنف أن يرث أو ينفّذ | واحدًا | كثيرًا |
| حقول المثيل (الحالة) | نعم | لا |
| المُنشئات | نعم | لا |
| دوال بجسم | نعم | نعم، منذ C# 8 (دوال الواجهة الافتراضية) |
| الأعضاء الساكنة | نعم | نعم، منذ C# 8؛ والأعضاء الساكنة المجرّدة منذ C# 11 |
| معدّلات الوصول على الأعضاء | أي معدّل | عامة افتراضيًا؛ وغيرها مسموح منذ C# 8 |
| تستطيع البنى استخدامه | لا | نعم |
| إضافة عضو لاحقًا | آمنة إن لم يكن مجرّدًا | تكسر المنفّذين، ما لم يكن له جسم افتراضي |
قلّصت دوال الواجهة الافتراضية (C# 8) الفجوة، لكن لا الفرق الجوهري:
public interface ILogger
{
void Write(string message);
void Error(string message) => Write("ERROR: " + message); // default implementation: C# 8, .NET Core 3.0+
}
ما زالت الدالة الافتراضية لا تستطيع لمس حقول المثيل، لأن الواجهة بلا حقول؛ ولا تستطيع إلا استدعاء أعضاء أخرى في الواجهة.
طريقة عملية للاختيار:
- واجهة حين تصف قدرة يمكن لأنواع غير مترابطة امتلاكها (
IComparable<T>وIDisposableوIShippingProvider)، أو حين يجب أن تستطيع البنى المشاركة، أو حين يحتاج صنف إلى عدة أدوار من هذا النوع. معظم شيفرات حقن الاعتماديات مكتوبة مقابل واجهات لهذا السبب. - صنف مجرّد حين تكون الأنواع المشتقة تنويعات حقيقية على شيء واحد وتتشارك حالة أو خوارزمية ثابتة، مثل أدوات التصدير أعلاه.
- الاثنان شائع: واجهة يعتمد عليها المستدعون، وصنف أساس مجرّد ينفّذها ويوفّر على المنفّذين إعادة كتابة الأجزاء المشتركة. تغطي صفحة الواجهات جانب الواجهة بالتفصيل.
أخطاء شائعة
- محاولة إنشاء مثيل من الصنف الأساس.
new Shape(...)هو CS0144. أنشئ مثيلًا من صنف مشتق. - نسيان
overrideفي التنفيذ. كتابةpublic double Area()في الصنف المشتق لا تنفّذ العضو المجرّد؛ يبلّغ المترجم بـ CS0534 للتنفيذ المفقود، إضافة إلى تحذير بأن الدالة الجديدة تخفي الموروثة. - مُنشئ عام في صنف مجرّد. غير ضار لكنه مضلّل. استخدم
protected. - أصناف مجرّدة بلا شيفرة مشتركة. إن لم يكن في الصنف الأساس إلا أعضاء مجرّدة ولا حالة، فالواجهة تقول الشيء نفسه ولا تستهلك خانة الصنف الأساس الوحيدة.
- استدعاء أعضاء مجرّدة من مُنشئ الأساس. يُنفَّذ التنفيذ المشتق قبل جسم المُنشئ المشتق، فتظل الحقول التي يسندها ذلك الجسم
0أوnull.
الأسئلة الشائعة
ما هو الصنف المجرّد في C#؟
الصنف المعلّم بـ abstract مقصود كصنف أساس فقط: لا يمكنك إنشاء مثيل منه بـ new (الخطأ CS0144). يمكن أن يحتوي على حقول ومُنشئات ودوال عادية، إضافة إلى أعضاء abstract بلا جسم يجب أن ينفّذها كل صنف مشتق غير مجرّد.
ما الفرق بين الصنف المجرّد والواجهة في C#؟
يمكن للصنف أن يشتق من صنف مجرّد واحد فقط لكنه ينفّذ واجهات كثيرة. يمكن للصنف المجرّد أن يحمل حالة (حقول مثيل) ومُنشئات وأعضاء بأي مستوى وصول، وهو المكان الطبيعي للتنفيذ المشترك. أما الواجهة فتصف قدرة بلا حالة مثيل؛ ومنذ C# 8 يمكن أن تتضمن أجسامًا افتراضية للدوال، لكنها ما زالت لا تملك حقول مثيل ولا مُنشئات.
ما الفرق بين الدوال المجرّدة والافتراضية في C#؟
للدالة virtual جسم يمكن للأصناف المشتقة تجاوزه. أما الدالة abstract فلا جسم لها ويجب على الأصناف المشتقة تجاوزها، وإلا تكون هي نفسها مجرّدة (الخطأ CS0534 لصنف فعلي). ولا يمكن أن تظهر الدوال المجرّدة إلا في أصناف مجرّدة.
هل يمكن أن يملك الصنف المجرّد مُنشئًا في C#؟
نعم. يُنفَّذ حين يُنشأ صنف مشتق، عبر : base(...)، ويهيّئ عادة الحقول المشتركة. صرّح به protected: المُنشئ العام في صنف مجرّد يوحي بأن المستدعين يستطيعون استخدامه، وهم لا يستطيعون.
لماذا لا أستطيع إنشاء مثيل من صنف مجرّد؟
لأنه قد يملك أعضاء مجرّدة بلا تنفيذ: استدعاء new Shape().Area() لن يجد شيفرة يشغّلها. يبلّغ المترجم بـ CS0144 ("Cannot create an instance of the abstract type or interface"). أنشئ مثيلًا من صنف مشتق فعلي بدلًا من ذلك، واحمله في متغيّر من النوع المجرّد إن شئت: Shape s = new Circle(2);.