C#: الفئات المجردة والواجهات
1. الفئات المجردة والطرق المجردة
يتم إعلان الفئات المجردة باستخدام المُعدِّل abstract ولا يمكن إنشاء مثيلات منها. ويمكن أن تحتوي على كل من الطرق المجردة والطرق الملموسة. ولا تحتوي الطرق المجردة على نص طريقة، ويجب تجاوزها في الفئات المشتقة.
▶ مثال
abstract class Shape
{
public string Name { get; set; }
public abstract double Area();
public void Print()
{
Console.WriteLine($"{Name} Area: {Area():F2}");
}
}
class Circle : Shape
{
public double Radius { get; set; }
public Circle(double radius)
{
Name = "Circle";
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 w, double h)
{
Name = "Rectangle";
Width = w;
Height = h;
}
public override double Area()
{
return Width * Height;
}
}
Circle c = new Circle(3);
Rectangle r = new Rectangle(4, 5);
c.Print();
r.Print();
Circle Area: 28.27
Rectangle Area: 20.00
💡 الفئة المجردة تشبه «قالبًا نصف مكتمل» — فهي تحدد ما يجب على الفئات الفرعية إكماله (الطرق المجردة)، كما توفر أجزاءً يمكن استخدامها مباشرةً (الطرق الملموسة).
2. الفئات المجردة مقابل الفئات العادية
| ميزة | فئة عادية | فئة مجردة |
|---|---|---|
| التمثيل | نعم | لا |
| الطرق المجردة | لا يمكن أن تحتوي على | يمكن أن تحتوي على |
| طرق محددة | نعم | نعم |
| المُنشئ | نعم | نعم |
| الحقول | نعم | نعم |
| عند التوريث | يمكن تجاوز الدالة الافتراضية | يجب تجاوز الدالة المجردة |
| مُعدِّل «sealed» | نعم | لا (متناقض) |
▶ مثال
abstract class Logger
{
protected string prefix;
public Logger(string prefix)
{
this.prefix = prefix;
}
public abstract void Log(string message);
public void LogWithTimestamp(string message)
{
string ts = DateTime.Now.ToString("HH:mm:ss");
Log($"[{ts}] {message}");
}
}
class FileLogger : Logger
{
public FileLogger() : base("FILE") { }
public override void Log(string message)
{
Console.WriteLine($"{prefix}: {message}");
}
}
FileLogger fl = new FileLogger();
fl.Log("Service started");
fl.LogWithTimestamp("Database connected");
FILE: Service started
FILE: [14:30:00] Database connected
⚠️ يمكن أن تحتوي الفئات المجردة على منشئات، لكن لا يمكن أن تكون المنشئات abstract. يتم استدعاء المنشئات عبر base() عند إنشاء مثيل لفئة فرعية.
3. تعريف الواجهة وتنفيذها
يتم تعريف الواجهات باستخدام الكلمة الرئيسية interface، وهي تمثل عقدًا لأعضاء الواجهة. ويجب على الفئات التي تنفذ واجهة ما أن توفر تطبيقات لجميع أعضائها. وتكون أعضاء الواجهة ضمنيًا public ولا يمكن أن تحتوي على مُعدِّلات الوصول.
▶ مثال
interface IMovable
{
void Move(double dx, double dy);
double Speed { get; set; }
}
interface IDrawable
{
void Draw();
}
class Player : IMovable, IDrawable
{
public double X { get; set; }
public double Y { get; set; }
public double Speed { get; set; }
public Player(double x, double y, double speed)
{
X = x;
Y = y;
Speed = speed;
}
public void Move(double dx, double dy)
{
X += dx * Speed;
Y += dy * Speed;
Console.WriteLine($"Moved to ({X:F1}, {Y:F1})");
}
public void Draw()
{
Console.WriteLine($"Drawing player @ ({X:F1}, {Y:F1})");
}
}
Player p = new Player(0, 0, 2.0);
p.Draw();
p.Move(3, 4);
p.Draw();
Drawing player @ (0.0, 0.0)
Moved to (6.0, 8.0)
Drawing player @ (6.0, 8.0)
📌 يجب أن تبدأ أسماء الواجهات بحرف كبير I، مثل IComparable وIDisposable — وهذا هو أسلوب التسمية المتبع في لغة C#.
4. تنفيذ واجهات متعددة
يمكن للفئة أن تنفذ واجهات متعددة، مفصولة بفواصل. وهذه إحدى الطرق التي تحقق بها لغة C# التعدد الشكلي، لتعويض قيود الوراثة الأحادية.
▶ مثال
interface IWalkable
{
void Walk();
}
interface ISwimmable
{
void Swim();
}
interface IFlyable
{
void Fly();
}
class Duck : IWalkable, ISwimmable, IFlyable
{
public string Name { get; set; }
public Duck(string name)
{
Name = name;
}
public void Walk()
{
Console.WriteLine($"{Name} waddles along");
}
public void Swim()
{
Console.WriteLine($"{Name} swims in the water");
}
public void Fly()
{
Console.WriteLine($"{Name} flaps its wings and takes off");
}
}
Duck d = new Duck("Donald");
d.Walk();
d.Swim();
d.Fly();
IWalkable walker = d;
walker.Walk();
ISwimmable swimmer = d;
swimmer.Swim();
Donald waddles along
Donald swims in the water
Donald flaps its wings and takes off
Donald waddles along
Donald swims in the water
💡 لا يمكن للمتغيرات من أنواع الواجهات المختلفة استدعاء سوى العناصر المحددة في تلك الواجهة — وهذا يعكس مبدأ فصل الواجهات.
5. الطرق الافتراضية للواجهات (C# 8)
ابتداءً من الإصدار C# 8، يمكن أن تحتوي الواجهات على طرق ذات تطبيقات افتراضية. وهذا يتيح إضافة عناصر جديدة إلى الواجهة دون التأثير سلبًا على الفئات التي تنفذها حاليًا.
▶ مثال
interface ILogger
{
void Log(string message);
void LogWarning(string message)
{
Log($"[WARN] {message}");
}
void LogError(string message)
{
Log($"[ERROR] {message}");
}
}
class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
class SimpleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($">> {message}");
}
}
ConsoleLogger cl = new ConsoleLogger();
cl.Log("System started");
cl.LogWarning("Low memory");
cl.LogError("Connection failed");
SimpleLogger sl = new SimpleLogger();
sl.Log("System started");
sl.LogWarning("Low memory");
System started
[WARN] Low memory
[ERROR] Connection failed
>> System started
>> [WARN] Low memory
⚠️ لا تتوفر الطرق الافتراضية إلا عند استدعائها من خلال نوع الواجهة. ولا يمكن استدعاؤها من خلال متغير من نوع الفئة المنفذة (ما لم تقم الفئة المنفذة أيضًا بإعادة تنفيذ الطريقة).
6. دليل اختيار الفئة المجردة مقابل الواجهة
| المقارنة | الفئة المجردة | الواجهة |
|---|---|---|
| الوراثة/التنفيذ | الوراثة الأحادية | التنفيذ المتعدد |
| أنواع الأعضاء | أي | الطرق، الخصائص، الأحداث، المُفهرِّسات |
| الحقول | نعم | لا (قبل الإصدار C# 8) |
| المُنشئ | نعم | لا |
| مُعدِّلات الوصول | أي | افتراضيًا: public |
| التنفيذ الافتراضي | نعم | منذ الإصدار C# 8 |
| التوافق بين الإصدارات | إضافة العناصر آمنة | إضافة العناصر تؤدي إلى تعطل عمليات التنفيذ (قبل C# 8) |
| الهدف من التصميم | "ما هو" (IS-A) | "ما يمكنه فعله" (CAN-DO) |
إرشادات الاختيار:
- تمثل علاقة هرمية من نوع «هو نوع من» → فئة مجردة
- تمثل القدرة أو السلوك من النوع المتقاطع ← واجهة
- الحاجة إلى مشاركة الكود والحالة ← الفئة المجردة
- الحاجة إلى إمكانية التوريث المتعدد ← الواجهة
- الحاجة إلى إضافة طرق جديدة إلى واجهة موجودة دون الإخلال بالرمز القديم ← الطريقة الافتراضية للواجهة
🔥 قاعدة سريعة: استخدم الفئات المجردة للهوية المشتركة، واستخدم الواجهات للقدرات المشتركة.
7. التنفيذ الصريح للواجهة
عندما تحتوي واجهات متعددة على عناصر تحمل الاسم نفسه، أو عندما تحتاج إلى إخفاء عناصر الواجهة، استخدم التنفيذ الصريح للواجهة. وتكون صيغة ذلك return_type InterfaceName.MemberName. ولا يمكن أن تحتوي العناصر المنفذة بشكل صريح على مُعدِّلات الوصول، ولا يمكن استدعاؤها إلا من خلال نوع الواجهة.
▶ مثال
interface IEnglish
{
void Speak();
}
interface IFrench
{
void Speak();
}
class BilingualPerson : IEnglish, IFrench
{
void IEnglish.Speak()
{
Console.WriteLine("Hello!");
}
void IFrench.Speak()
{
Console.WriteLine("Bonjour!");
}
public void Speak()
{
Console.WriteLine("Hi!");
}
}
BilingualPerson p = new BilingualPerson();
p.Speak();
IEnglish eng = p;
eng.Speak();
IFrench fr = p;
fr.Speak();
Hi!
Hello!
Bonjour!
📌 لا تظهر عناصر واجهة البرمجة التي تم تنفيذها صراحةً في مثيل الفئة — لذا يجب عليك تحويل النوع إلى نوع الواجهة المقابلة لاستدعائها. ويُعد هذا في الوقت نفسه قيدًا وآلية تغليف.
8. تطبيق IComparable<T> عمليًّا
تعد IComparable<T> إحدى الواجهات الأكثر استخدامًا في .NET، وتُستخدم لتحديد ترتيب الفرز الطبيعي لنوع ما. وتعتمد كل من Array.Sort وList<T>.Sort` على هذه الواجهة.
▶ مثال
class Student : IComparable<Student>
{
public string Name { get; set; }
public int Score { get; set; }
public Student(string name, int score)
{
Name = name;
Score = score;
}
public int CompareTo(Student other)
{
if (other == null) return 1;
return Score.CompareTo(other.Score);
}
public override string ToString()
{
return $"{Name}({Score})";
}
}
Student[] students = new Student[]
{
new Student("Alice", 85),
new Student("Bob", 92),
new Student("Charlie", 78),
new Student("Diana", 95),
new Student("Eve", 88)
};
Console.WriteLine("Before sorting:");
foreach (var s in students)
Console.WriteLine(s);
Array.Sort(students);
Console.WriteLine("After sorting (by score ascending):");
foreach (var s in students)
Console.WriteLine(s);
Before sorting:
Alice(85)
Bob(92)
Charlie(78)
Diana(95)
Eve(88)
After sorting (by score ascending):
Charlie(78)
Alice(85)
Eve(88)
Bob(92)
Diana(95)
💻 CompareTo قاعدة تحديد قيمة الإرجاع: القيمة السالبة تعني أن الكائن الحالي يأتي قبل الكائن الآخر، والصفر يعني التساوي، والقيمة الموجبة تعني أن الكائن الحالي يأتي بعده. باستخدام IComparable<T>، يمكن للأنواع المخصصة استخدام Array.Sort وList<T>.Sort` وميزات الفرز الأخرى مباشرةً.
❓ أسئلة شائعة
📖 ملخص
- لا يمكن إنشاء مثيلات للفئات
abstract، ويمكن أن تحتوي على كل من الطرق المجردة والمحددة - الطرق المجردة لا تحتوي على نص؛ ويجب على الفئات المشتقة تجاوزها باستخدام
override - تحدد الواجهات العقود؛ ويجب أن توفر الفئات المنفذة تطبيقات لجميع العناصر
- يمكن للفئة أن تنفذ واجهات متعددة، مما يعوض عن قيود الوراثة الأحادية
- تدعم واجهات C# 8 الطرق الافتراضية من أجل التوافق مع الإصدارات السابقة
- تمثل الفئات المجردة «ماهيتها»؛ بينما تمثل الواجهات «ما يمكنها فعله»
- يعمل التنفيذ الصريح للواجهة على حل تعارضات التسمية، ولا يمكن استدعاؤه إلا من خلال نوع الواجهة
- يحدد
IComparable<T>الترتيب الطبيعي ويعمل مع أساليبSort
📝 تمارين
- قم بتعريف فئة مجردة
Vehicleتحتوي على طريقة مجردةStart()وطريقة ملموسةStop()، ثم أنشئ الفئتين الفرعيتينCarوBikeلتنفيذها - قم بتعريف واجهة
ISerializableتحتوي على الطريقةstring Serialize()، وواجهةIDeserializableتحتوي على الطريقةvoid Deserialize(string data)، ثم أنشئ فئة تُنفِّذ كلتا الواجهتين - قم بتعريف واجهتين
IPlayableوIRecordable، تحتوي كلتاهما على طريقةvoid Pause()، واستخدم التنفيذ الصريح للواجهة للتمييز بينهما - أنشئ فئة
Productالتي تنفذIComparable<Product>للفرز حسب السعر، واختبرها باستخدامArray.Sort - أضف طريقة
void PrintHeader(string title)مع تنفيذ افتراضي إلى واجهةIPrintableالموجودة، وتأكد من أن الفئات التي كانت تنفذها سابقًا لم تتأثر بذلك