C#: التعدد الأشكال

1. نظرة عامة على تعدد الأشكال

يُعد التعدد الشكلي إحدى السمات الأساسية للبرمجة الموجهة للكائنات، ويشير إلى قدرة واجهة واحدة على إظهار سلوكيات مختلفة على كائنات مختلفة. في لغة C#، يتخذ التعدد الشكلي شكلين:

يركز هذا الدرس على تعدد الأشكال في وقت التشغيل، وهو أهم ميزة في التصميم الموجه للكائنات.


2. الافتراضي والتجاوز

تُشير الفئة الأساسية إلى أن إحدى الطرق قابلة للتجاوز باستخدام virtual، بينما توفر الفئة المشتقة تنفيذًا جديدًا باستخدام override. وفي وقت التشغيل، يعتمد استدعاء الطريقة على النوع الفعلي للكائن (وليس النوع المُعلن للمتغير) — وهذا ما يُعرف بالتوزيع الديناميكي.

▶ مثال

CSHARP
using System;

class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal makes a sound");
    }
}

class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Dog barks");
    }
}

class Cat : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Cat meows");
    }
}

class Program
{
    static void Main()
    {
        Animal a = new Dog();
        a.Speak();

        Animal b = new Cat();
        b.Speak();
    }
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Dog barks
Cat meows

تم تعريف المتغيرات على أنها Animal، لكن الكائنات الفعلية هي Dog وCat. وأثناء وقت التشغيل، يتم استدعاء كل نسخة تم تجاوزها — وهذا هو التأثير الأساسي للتعدد الشكلي.


3. التطبيق العملي: واجهة موحدة، تطبيقات مختلفة

السيناريو الأكثر شيوعًا لتعدد الأشكال هو استخدام مصفوفة أو مجموعة من مراجع الفئة الأساسية لإدارة كائنات مختلفة من الفئات المشتقة، مع استدعاء واجهة موحدة بينما يستجيب كل كائن بطريقته الخاصة.

▶ مثال

CSHARP 📖 للعرض فقط
using System;

class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal makes a sound");
    }
}

class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Dog barks");
    }
}

class Cat : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Cat meows");
    }
}

class Cow : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Cow moos");
    }
}

class Program
{
    static void MakeThemSpeak(Animal[] animals)
    {
        foreach (Animal a in animals)
        {
            a.Speak();
        }
    }

    static void Main()
    {
        Animal[] animals = { new Dog(), new Cat(), new Cow() };
        MakeThemSpeak(animals);
    }
}
44 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)
TEXT 📖 للعرض فقط
Dog barks
Cat meows
Cow moos

تعتمد الطريقة MakeThemSpeak على النوع Animal فقط. فهي لا تحتاج إلى معرفة نوع الحيوان المحدد الذي تتعامل معه، كما أن إضافة فئات مشتقة جديدة لا تتطلب أي تعديل على هذه الطريقة.


4. override مقابل new

يمكن أن يؤدي استخدام الكلمة الرئيسية new في فئة مشتقة إلى إخفاء إحدى طرق الفئة الأساسية، لكن هذا لا يدخل في نطاق تعدد الأشكال. تشارك override في التوزيع الديناميكي، بينما يعمل إخفاء new على اختيار الطريقة في وقت التحويل البرمجي بناءً على النوع المُعلن.

▶ مثال

CSHARP
using System;

class Base
{
    public virtual void Print()
    {
        Console.WriteLine("Base.Print");
    }
}

class OverrideDerived : Base
{
    public override void Print()
    {
        Console.WriteLine("OverrideDerived.Print");
    }
}

class NewDerived : Base
{
    public new void Print()
    {
        Console.WriteLine("NewDerived.Print");
    }
}

class Program
{
    static void Main()
    {
        Base obj1 = new OverrideDerived();
        obj1.Print();

        Base obj2 = new NewDerived();
        obj2.Print();

        NewDerived obj3 = new NewDerived();
        obj3.Print();
    }
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
OverrideDerived.Print
Base.Print
NewDerived.Print
💡 استخدم دائمًا override عندما تحتاج إلى سلوك متعدد الأشكال. ولا تستخدم new إلا لإخفاء الطرق في حالات خاصة.


5. الطرق المجردة والفئات المجردة

الطريقة abstract لا تحتوي على تنفيذ، وتجبر الفئات المشتقة على تجاوزها. ويجب أن تُعلن الفئة التي تحتوي على طرق مجردة بأنها abstract، ولا يمكن إنشاء مثيل لها مباشرةً.

▶ مثال

CSHARP 📖 للعرض فقط
using System;

abstract class Shape
{
    public abstract double Area();
}

class Circle : Shape
{
    public double Radius { get; set; }

    public Circle(double radius)
    {
        Radius = radius;
    }

    public override double Area()
    {
        return Math.PI * Radius * Radius;
    }
}

class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public override double Area()
    {
        return Width * Height;
    }
}

class Program
{
    static void PrintArea(Shape shape)
    {
        Console.WriteLine($"Area: {shape.Area():F2}");
    }

    static void Main()
    {
        Shape[] shapes = { new Circle(3), new Rectangle(4, 5) };
        foreach (Shape s in shapes)
        {
            PrintArea(s);
        }
    }
}
46 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)
TEXT 📖 للعرض فقط
Area: 28.27
Area: 20.00

تحدد الفئة المجردة «ماهيتها»، بينما تحدد الفئات المشتقة «كيفية عملها». وتحسب جميع الأشكال مساحتها من خلال واجهة Area() الموحدة.


6. تسلسل الطرق: التجاوز متعدد المستويات

يمكن تجاوز virtual/override طبقةً تلو الأخرى على طول سلسلة الوراثة. ويمكن للفئة المشتقة استدعاء التنفيذ الخاص بالفئة الأم عبر base. وتوسيعه بشكل أكبر.

▶ مثال

CSHARP
using System;

class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal speaks");
    }
}

class Dog : Animal
{
    public override void Speak()
    {
        base.Speak();
        Console.WriteLine("Dog barks");
    }
}

class Puppy : Dog
{
    public override void Speak()
    {
        base.Speak();
        Console.WriteLine("Puppy yips");
    }
}

class Program
{
    static void Main()
    {
        Animal a = new Puppy();
        a.Speak();
    }
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Animal speaks
Dog barks
Puppy yips

تقوم Puppy.Speak() باستدعاء base.Speak()، مما يؤدي إلى تشغيل Dog.Speak()، التي تقوم بدورها باستدعاء base.Speak()، مما يؤدي إلى تشغيل Animal.Speak()، لتشكل بذلك سلسلة من المخرجات من الفئة الأساسية إلى الفئة المشتقة.


7. تجاوز طريقة ToString

ترث جميع الأنواع في لغة C# من object، وتكون طريقة ToString() الخاصة بها هي virtual. ويتيح تجاوز هذه الطريقة للكائن عرض نص ذي معنى عند الإخراج.

▶ مثال

CSHARP
using System;

class Student
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Student(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public override string ToString()
    {
        return $"{Name} (Age: {Age})";
    }
}

class Program
{
    static void Main()
    {
        Student s = new Student("Alice", 20);
        Console.WriteLine(s);
        Console.WriteLine(s.ToString());
    }
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Alice (Age: 20)
Alice (Age: 20)

يقوم Console.WriteLine باستدعاء ToString() تلقائيًا داخليًّا. وبعد التجاوز، يتغير الناتج من اسم النوع الافتراضي إلى تنسيق مخصص.


8. مبدأ الاستبدال لدى ليسكوف

يُعد مبدأ استبدال ليسكوف (LSP) أحد المبادئ الأساسية للتصميم الموجه للكائنات: يجب أن تكون كائنات الفئة المشتقة قادرة على استبدال كائنات الفئة الأساسية دون المساس بصحة البرنامج.

⚠️ انتهاك نموذجي لمبدأ LSP: تقوم فئة مشتقة بتجاوز طريقة ما وتُطلق استثناءً لم يُعلن عنه في الفئة الأساسية، أو تُرجع نتيجة تتعارض مع عقد الفئة الأساسية.

▶ مثال

CSHARP
using System;

class Bird
{
    public virtual void Fly()
    {
        Console.WriteLine("Bird flies");
    }
}

class Swallow : Bird
{
    public override void Fly()
    {
        Console.WriteLine("Swallow flies high");
    }
}

class Penguin : Bird
{
    public override void Fly()
    {
        throw new NotSupportedException("Penguin cannot fly");
    }
}

class Program
{
    static void LetFly(Bird bird)
    {
        bird.Fly();
    }

    static void Main()
    {
        LetFly(new Swallow());
        LetFly(new Penguin());
    }
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Swallow flies high

Unhandled Exception: System.NotSupportedException: Penguin cannot fly

يرث Penguin من Bird، لكنه يتجاوز Fly لإلقاء استثناء، مما يكسر عقد «الطيور تستطيع الطيران» الخاص بالفئة الأساسية وينتهك مبدأ LSP. الطريقة الصحيحة هي إعادة تصميم التسلسل الهرمي للوراثة، مثل استخراج «القدرة على الطيران» إلى واجهة منفصلة.


❓ أسئلة شائعة

س هل يمكن ترك طريقة virtual دون استبدال؟
ج نعم. إذا لم يتم استبدالها، يتم استخدام التنفيذ الافتراضي للفئة الأساسية.
س هل يمكن تجاوز طريقة override مرة أخرى؟
ج نعم. تحمل طريقة override ضمنيًا دلالات virtual، لذا يمكن للفئات المشتقة اللاحقة الاستمرار في تجاوزها.
س أيهما أفضل، new أم override؟
ج استخدم override عندما تحتاج إلى سلوك متعدد الأشكال. استخدم new فقط عندما تضطر إلى إخفاء إحدى الطرق وتكون متأكدًا من عدم الحاجة إلى تعدد الأشكال.
س ما الفرق بين الفئة المجردة والواجهة؟
ج يمكن أن تحتوي الفئة المجردة على حقول ورمز تنفيذ؛ أما الواجهة فتقتصر على تعريف العقد فقط. ولا يمكن للفئة أن ترث إلا من فئة مجردة واحدة، لكن يمكنها تنفيذ واجهات متعددة.
س ماذا يحدث إذا لم أقم بتجاوز ToString()؟
ج يعرض الإعداد الافتراضي اسم النوع الكامل؛ على سبيل المثال، سيؤدي Student إلى إخراج Student.

📖 ملخص

📝 تمارين

  1. قم بتعريف فئة أساسية Vehicle تحتوي على دالة virtual void Move()، ثم قم باشتقاق فئتي Car وBicycle اللتين تتجاوزان Move()، واستدعاءها بشكل موحد من خلال مصفوفة Vehicle[] في Main
  2. قم بتعريف فئة مجردة Payment تحتوي على طريقة abstract void Pay(decimal amount)، ثم قم بإنشاء فئتي CashPayment وCardPayment اللتين تنفذان منطق الدفع الخاص بهما
  3. أنشئ فئة Book وقم بتجاوز ToString() لعرض عنوان الكتاب وسعره؛ وتحقق من نتيجة Console.WriteLine(book)
  4. اكتب كودًا يوضح الفرق بين override وnew: استخدم مرجعًا للفئة الأساسية لاستدعاء كائنين من الفئتين المشتقتين على التوالي، ولاحظ الفرق في النتائج
  5. فكر في الأمر التالي: إذا كان Square يرث من Rectangle ولكنه يتجاوز SetWidth/SetHeight بحيث تكون أطوال الأضلاع متساوية دائمًا، فهل هذا يخالف مبدأ الاستبدال ليسكوف؟ اشرح أسبابك
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%