C#: التعدد الأشكال
1. نظرة عامة على تعدد الأشكال
يُعد التعدد الشكلي إحدى السمات الأساسية للبرمجة الموجهة للكائنات، ويشير إلى قدرة واجهة واحدة على إظهار سلوكيات مختلفة على كائنات مختلفة. في لغة C#، يتخذ التعدد الشكلي شكلين:
- التعدد الشكلي في وقت التشغيل: يتم تحقيقه من خلال
virtual/overrideمن أجل التوجيه الديناميكي؛ حيث يتم تحديد الطريقة التي سيتم استدعاؤها في وقت التشغيل - التعدد الشكلي في وقت التحويل البرمجي: يتحقق من خلال تحميل الطرق، حيث يختار المُترجم هدف الاستدعاء بناءً على توقيع المعلمة (تم تناوله في الدرس 14)
يركز هذا الدرس على تعدد الأشكال في وقت التشغيل، وهو أهم ميزة في التصميم الموجه للكائنات.
2. الافتراضي والتجاوز
تُشير الفئة الأساسية إلى أن إحدى الطرق قابلة للتجاوز باستخدام virtual، بينما توفر الفئة المشتقة تنفيذًا جديدًا باستخدام override. وفي وقت التشغيل، يعتمد استدعاء الطريقة على النوع الفعلي للكائن (وليس النوع المُعلن للمتغير) — وهذا ما يُعرف بالتوزيع الديناميكي.
▶ مثال
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();
}
}
Dog barks
Cat meows
تم تعريف المتغيرات على أنها Animal، لكن الكائنات الفعلية هي Dog وCat. وأثناء وقت التشغيل، يتم استدعاء كل نسخة تم تجاوزها — وهذا هو التأثير الأساسي للتعدد الشكلي.
3. التطبيق العملي: واجهة موحدة، تطبيقات مختلفة
السيناريو الأكثر شيوعًا لتعدد الأشكال هو استخدام مصفوفة أو مجموعة من مراجع الفئة الأساسية لإدارة كائنات مختلفة من الفئات المشتقة، مع استدعاء واجهة موحدة بينما يستجيب كل كائن بطريقته الخاصة.
▶ مثال
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);
}
}
Dog barks
Cat meows
Cow moos
تعتمد الطريقة MakeThemSpeak على النوع Animal فقط. فهي لا تحتاج إلى معرفة نوع الحيوان المحدد الذي تتعامل معه، كما أن إضافة فئات مشتقة جديدة لا تتطلب أي تعديل على هذه الطريقة.
4. override مقابل new
يمكن أن يؤدي استخدام الكلمة الرئيسية new في فئة مشتقة إلى إخفاء إحدى طرق الفئة الأساسية، لكن هذا لا يدخل في نطاق تعدد الأشكال. تشارك override في التوزيع الديناميكي، بينما يعمل إخفاء new على اختيار الطريقة في وقت التحويل البرمجي بناءً على النوع المُعلن.
▶ مثال
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();
}
}
OverrideDerived.Print
Base.Print
NewDerived.Print
obj1: تجاوزoverride— حتى من خلال مرجع إلى الفئة الأساسية، يتم استدعاء النسخة الموجودة في الفئة المشتقةobj2:newإخفاء — من خلال مرجع إلى الفئة الأساسية، يتم استدعاء نسخة الفئة الأساسية؛ مما يؤدي إلى تعطيل تعدد الأشكالobj3: لا يمكن الوصول إلى النسخة المخفيةnewإلا من خلال مرجع لفئة مشتقة
override عندما تحتاج إلى سلوك متعدد الأشكال. ولا تستخدم new إلا لإخفاء الطرق في حالات خاصة.
5. الطرق المجردة والفئات المجردة
الطريقة abstract لا تحتوي على تنفيذ، وتجبر الفئات المشتقة على تجاوزها. ويجب أن تُعلن الفئة التي تحتوي على طرق مجردة بأنها abstract، ولا يمكن إنشاء مثيل لها مباشرةً.
▶ مثال
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);
}
}
}
Area: 28.27
Area: 20.00
تحدد الفئة المجردة «ماهيتها»، بينما تحدد الفئات المشتقة «كيفية عملها». وتحسب جميع الأشكال مساحتها من خلال واجهة Area() الموحدة.
6. تسلسل الطرق: التجاوز متعدد المستويات
يمكن تجاوز virtual/override طبقةً تلو الأخرى على طول سلسلة الوراثة. ويمكن للفئة المشتقة استدعاء التنفيذ الخاص بالفئة الأم عبر base. وتوسيعه بشكل أكبر.
▶ مثال
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();
}
}
Animal speaks
Dog barks
Puppy yips
تقوم Puppy.Speak() باستدعاء base.Speak()، مما يؤدي إلى تشغيل Dog.Speak()، التي تقوم بدورها باستدعاء base.Speak()، مما يؤدي إلى تشغيل Animal.Speak()، لتشكل بذلك سلسلة من المخرجات من الفئة الأساسية إلى الفئة المشتقة.
7. تجاوز طريقة ToString
ترث جميع الأنواع في لغة C# من object، وتكون طريقة ToString() الخاصة بها هي virtual. ويتيح تجاوز هذه الطريقة للكائن عرض نص ذي معنى عند الإخراج.
▶ مثال
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());
}
}
Alice (Age: 20)
Alice (Age: 20)
يقوم Console.WriteLine باستدعاء ToString() تلقائيًا داخليًّا. وبعد التجاوز، يتغير الناتج من اسم النوع الافتراضي إلى تنسيق مخصص.
8. مبدأ الاستبدال لدى ليسكوف
يُعد مبدأ استبدال ليسكوف (LSP) أحد المبادئ الأساسية للتصميم الموجه للكائنات: يجب أن تكون كائنات الفئة المشتقة قادرة على استبدال كائنات الفئة الأساسية دون المساس بصحة البرنامج.
▶ مثال
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());
}
}
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.📖 ملخص
- يتخذ التعدد أشكالين: شكل وقت التشغيل (
virtual/override) وشكل وقت التحويل البرمجي (التحميل الزائد) virtualتشير إلى طريقة قابلة للتجاوز؛overrideتوفر تنفيذًا جديدًا في الفئة المشتقةabstractتُجبر هذه الأساليب الفئات المشتقة على تجاوزها؛ ولا يمكن إنشاء مثيلات للفئات المجردةoverrideتشارك في التوزيع الديناميكي؛newتخفي طريقة الفئة الأساسية وتعيق التعدد الشكلي- يُعد استدعاء طرق مختلفة لفئة مشتقة من خلال مرجع للفئة الأساسية أحد التطبيقات النموذجية للتعدد الأشكال
- ينص مبدأ الاستبدال ليسكوف على أن الفئات المشتقة يجب أن تكون قادرة على الاستبدال الكامل لفئاتها الأساسية
- تجاوز
ToString()هو أحد أكثر تطبيقات تعدد الأشكال شيوعًا في البرمجة اليومية
📝 تمارين
- قم بتعريف فئة أساسية
Vehicleتحتوي على دالةvirtual void Move()، ثم قم باشتقاق فئتيCarوBicycleاللتين تتجاوزانMove()، واستدعاءها بشكل موحد من خلال مصفوفةVehicle[]فيMain - قم بتعريف فئة مجردة
Paymentتحتوي على طريقةabstract void Pay(decimal amount)، ثم قم بإنشاء فئتيCashPaymentوCardPaymentاللتين تنفذان منطق الدفع الخاص بهما - أنشئ فئة
Bookوقم بتجاوزToString()لعرض عنوان الكتاب وسعره؛ وتحقق من نتيجةConsole.WriteLine(book) - اكتب كودًا يوضح الفرق بين
overrideوnew: استخدم مرجعًا للفئة الأساسية لاستدعاء كائنين من الفئتين المشتقتين على التوالي، ولاحظ الفرق في النتائج - فكر في الأمر التالي: إذا كان
Squareيرث منRectangleولكنه يتجاوزSetWidth/SetHeightبحيث تكون أطوال الأضلاع متساوية دائمًا، فهل هذا يخالف مبدأ الاستبدال ليسكوف؟ اشرح أسبابك