تخطَّ إلى المحتوى

الوراثة والواجهات في C#‎: abstract و interface

الدرس 9 من 11 · C#

في درس الكلاسات ورثت كلاساً من آخر باستخدام virtual و override. لكن تصميم البرامج والألعاب الحقيقية يحتاج أكثر: كلاس أب لا يُنشأ منه كائن أصلاً، وقدرات مشتركة بين كلاسات لا قرابة بينها. في هذا الدرس ستتعلم الكلاسات المجردة والواجهات، وهما أساس تصميم أي مشروع C#‎ كبير.

ما مساحة «شكل»؟ لا إجابة حتى نعرف هل هو دائرة أم مستطيل. الكلاس Shape فكرة عامة، ولا معنى لإنشاء كائن منه:

abstract class Shape
{
public string Name { get; }
protected Shape(string name)
{
Name = name;
}
public abstract double Area();
public void Print()
{
Console.WriteLine($"{Name}: {Area()}");
}
}
class Rectangle : Shape
{
private readonly double width, height;
public Rectangle(double width, double height) : base("مستطيل")
{
this.width = width;
this.height = height;
}
public override double Area() => width * height;
}
class Circle : Shape
{
private readonly double radius;
public Circle(double radius) : base("دائرة")
{
this.radius = radius;
}
public override double Area() => 3.14 * radius * radius;
}
Shape[] shapes = { new Rectangle(4, 5), new Circle(10) };
foreach (Shape shape in shapes)
{
shape.Print();
}
الناتج
مستطيل: 20
دائرة: 314
  • abstract class: كلاس لا يمكن إنشاء كائن منه. السطر new Shape("شكل") خطأ ترجمة.
  • abstract double Area();: دالة بلا جسم، وكل كلاس وارث ملزم بكتابتها بـ override.
  • الكلاس المجرد يحمل مع ذلك خصائص ودوالاً عادية مثل Print.
  • Print مكتوبة مرة واحدة في الأب، وتستدعي نسخة Area الخاصة بكل شكل. هذا هو تعدد الأشكال (Polymorphism).

الكلاس المجرد مثل قالب كعك بلا حشوة: الشكل العام جاهز، لكن لا تُقدَّم الكعكة حتى يختار كل طاهٍ حشوته.

في لعبة، اللاعب يتلقى الضرر، والصندوق الخشبي يتلقى الضرر، والجدار كذلك. لا قرابة بينها، لكن تجمعها قدرة واحدة. الواجهة (Interface) تصف قدرة يتعهد الكلاس بتنفيذها:

interface IDamageable
{
int Health { get; }
void TakeDamage(int amount);
}
interface IDescribable
{
string Describe();
}
class Crate : IDamageable, IDescribable
{
public int Health { get; private set; } = 30;
public void TakeDamage(int amount)
{
Health = Math.Max(0, Health - amount);
if (Health == 0) Console.WriteLine("تحطم الصندوق!");
}
public string Describe() => $"صندوق خشبي، صحته {Health}";
}
var crate = new Crate();
Console.WriteLine(crate.Describe());
IDamageable target = crate;
target.TakeDamage(20);
Console.WriteLine(target.Health);
target.TakeDamage(20);
الناتج
صندوق خشبي، صحته 30
10
تحطم الصندوق!
  • الكلاس يرث من كلاس أب واحد فقط، لكنه ينفّذ أي عدد من الواجهات مفصولة بفواصل.
  • أعضاء الواجهة public تلقائياً، وتنفيذها في الكلاس يُكتب public بدون override.
  • الواجهة قد تطلب دوالاً أو خصائص مثل Health.
  • العُرف في C#‎ أن يبدأ اسم الواجهة بالحرف I: مثل IDamageable و IComparable.

الواجهة مثل مقبس الكهرباء: أي جهاز له القابس المناسب يعمل، ولا يهم المقبس هل هو مصباح أم ثلاجة.

متغير من نوع الأب قد يحمل كائناً من أي ابن. الكلمة is تفحص النوع الحقيقي وتعرّف متغيراً منه في خطوة واحدة:

object[] items = { "نص", 42, 3.5 };
foreach (object item in items)
{
if (item is string s)
{
Console.WriteLine($"نص طوله {s.Length}");
}
else if (item is int n)
{
Console.WriteLine($"عدد صحيح ضعفه {n * 2}");
}
else
{
Console.WriteLine($"نوع آخر: {item}");
}
}
الناتج
نص طوله 2
عدد صحيح ضعفه 84
نوع آخر: 3.5

كل الأنواع في C#‎ ترث من object، ولذلك قبلت المصفوفة أي شيء. ويمكن كتابة الفحص نفسه بشكل أقصر بتعبير switch:

static string Describe(object item) => item switch
{
string s => $"نص طوله {s.Length}",
int n when n < 0 => "عدد سالب",
int n => $"عدد صحيح ضعفه {n * 2}",
null => "لا شيء",
_ => "نوع آخر",
};
Console.WriteLine(Describe("مرحباً"));
Console.WriteLine(Describe(-5));
Console.WriteLine(Describe(21));
Console.WriteLine(Describe(true));
الناتج
نص طوله 6
عدد سالب
عدد صحيح ضعفه 42
نوع آخر

الفروع تُفحص بالترتيب، و when تضيف شرطاً إضافياً، والشرطة السفلية _ تعني «أي شيء آخر».

كلمة sealed أمام كلاس تمنع أي كلاس آخر من الوراثة منه:

sealed class Token
{
public string Value { get; } = "abc123";
}

الكلاس string نفسه sealed، فلا يستطيع أحد أن يرث منه ويغيّر سلوكه.

الكلاس المجرد الواجهة
العلاقة «هو نوع من» «يستطيع أن»
العدد المسموح أب واحد أي عدد
تخزين بيانات نعم، حقول ومُنشئ لا تخزن حالة
مثال Shape و Circle IDamageable و IComparable

القاعدة العملية: ابدأ بالواجهة، واختر الكلاس المجرد فقط إذا كان الأبناء يشتركون في بيانات وكود فعلي.

  1. أنشئ abstract class Vehicle فيه خاصية Name ودالة مجردة Wheels() ودالة عادية Info().
  2. أنشئ Car و Bike يرثان منه، وضعهما في مصفوفة واطبع معلوماتهما.
  3. أنشئ interface IElectric فيها void Charge()، وكلاس ElectricCar يرث من Vehicle وينفّذ IElectric.
  4. تحدٍّ: مرّ على مصفوفة المركبات، واشحن فقط ما ينفّذ IElectric باستخدام is.
  • إنشاء كائن من كلاس مجرد: new Shape(...) يعطي: Cannot create an instance of the abstract type or interface 'Shape'.
  • نسيان override: كتابة الدالة في الابن بدون override لا تنفّذ الدالة المجردة، فيرفض المترجم الكلاس.
  • نسيان عضو من الواجهة: ستقرأ does not implement interface member. الواجهة عقد، وكل بنوده إلزامية.
  • تنفيذ غير public: أعضاء الواجهة عامة، فتنفيذها في الكلاس يجب أن يكون public.

سؤال

كم واجهة يستطيع الكلاس الواحد أن ينفّذ؟

سؤال

ماذا يحدث إذا ورث كلاس عادي من كلاس مجرد ولم يكتب دالته المجردة؟

  • abstract class لا يُنشأ منه كائن، ودواله المجردة يلتزم بها الأبناء بـ override.
  • interface تصف قدرة، واسمها يبدأ بـ I، والكلاس ينفّذ أي عدد منها.
  • تعدد الأشكال: الكائن الحقيقي هو من يحدد نسخة الدالة التي تعمل.
  • is وتعبير switch يفحصان النوع ويعرّفان متغيراً منه.
  • sealed تمنع الوراثة من الكلاس.