C#: الفئات المجردة والواجهات

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

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

▶ مثال

CSHARP 📖 للعرض فقط
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();
41 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)
TEXT 📖 للعرض فقط
Circle Area: 28.27
Rectangle Area: 20.00

💡 الفئة المجردة تشبه «قالبًا نصف مكتمل» — فهي تحدد ما يجب على الفئات الفرعية إكماله (الطرق المجردة)، كما توفر أجزاءً يمكن استخدامها مباشرةً (الطرق الملموسة).


2. الفئات المجردة مقابل الفئات العادية

ميزة فئة عادية فئة مجردة
التمثيل نعم لا
الطرق المجردة لا يمكن أن تحتوي على يمكن أن تحتوي على
طرق محددة نعم نعم
المُنشئ نعم نعم
الحقول نعم نعم
عند التوريث يمكن تجاوز الدالة الافتراضية يجب تجاوز الدالة المجردة
مُعدِّل «sealed» نعم لا (متناقض)

▶ مثال

CSHARP
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");
▶ جرّب الكود
TEXT 📖 للعرض فقط
FILE: Service started
FILE: [14:30:00] Database connected

⚠️ يمكن أن تحتوي الفئات المجردة على منشئات، لكن لا يمكن أن تكون المنشئات abstract. يتم استدعاء المنشئات عبر base() عند إنشاء مثيل لفئة فرعية.


3. تعريف الواجهة وتنفيذها

يتم تعريف الواجهات باستخدام الكلمة الرئيسية interface، وهي تمثل عقدًا لأعضاء الواجهة. ويجب على الفئات التي تنفذ واجهة ما أن توفر تطبيقات لجميع أعضائها. وتكون أعضاء الواجهة ضمنيًا public ولا يمكن أن تحتوي على مُعدِّلات الوصول.

▶ مثال

CSHARP
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();
▶ جرّب الكود
TEXT 📖 للعرض فقط
Drawing player @ (0.0, 0.0)
Moved to (6.0, 8.0)
Drawing player @ (6.0, 8.0)

📌 يجب أن تبدأ أسماء الواجهات بحرف كبير I، مثل IComparable وIDisposable — وهذا هو أسلوب التسمية المتبع في لغة C#.


4. تنفيذ واجهات متعددة

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

▶ مثال

CSHARP
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();
▶ جرّب الكود
TEXT 📖 للعرض فقط
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، يمكن أن تحتوي الواجهات على طرق ذات تطبيقات افتراضية. وهذا يتيح إضافة عناصر جديدة إلى الواجهة دون التأثير سلبًا على الفئات التي تنفذها حاليًا.

▶ مثال

CSHARP
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");
▶ جرّب الكود
TEXT 📖 للعرض فقط
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. ولا يمكن أن تحتوي العناصر المنفذة بشكل صريح على مُعدِّلات الوصول، ولا يمكن استدعاؤها إلا من خلال نوع الواجهة.

▶ مثال

CSHARP
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();
▶ جرّب الكود
TEXT 📖 للعرض فقط
Hi!
Hello!
Bonjour!

📌 لا تظهر عناصر واجهة البرمجة التي تم تنفيذها صراحةً في مثيل الفئة — لذا يجب عليك تحويل النوع إلى نوع الواجهة المقابلة لاستدعائها. ويُعد هذا في الوقت نفسه قيدًا وآلية تغليف.


8. تطبيق IComparable<T> عمليًّا

تعد IComparable<T> إحدى الواجهات الأكثر استخدامًا في .NET، وتُستخدم لتحديد ترتيب الفرز الطبيعي لنوع ما. وتعتمد كل من Array.Sort وList<T>.Sort` على هذه الواجهة.

▶ مثال

CSHARP
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);
▶ جرّب الكود
TEXT 📖 للعرض فقط
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` وميزات الفرز الأخرى مباشرةً.


❓ أسئلة شائعة

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

📖 ملخص

📝 تمارين

  1. قم بتعريف فئة مجردة Vehicle تحتوي على طريقة مجردة Start() وطريقة ملموسة Stop()، ثم أنشئ الفئتين الفرعيتين Car وBike لتنفيذها
  2. قم بتعريف واجهة ISerializable تحتوي على الطريقة string Serialize()، وواجهة IDeserializable تحتوي على الطريقة void Deserialize(string data)، ثم أنشئ فئة تُنفِّذ كلتا الواجهتين
  3. قم بتعريف واجهتين IPlayable وIRecordable، تحتوي كلتاهما على طريقة void Pause()، واستخدم التنفيذ الصريح للواجهة للتمييز بينهما
  4. أنشئ فئة Product التي تنفذ IComparable<Product> للفرز حسب السعر، واختبرها باستخدام Array.Sort
  5. أضف طريقة void PrintHeader(string title) مع تنفيذ افتراضي إلى واجهة IPrintable الموجودة، وتأكد من أن الفئات التي كانت تنفذها سابقًا لم تتأثر بذلك
Web-Tutorial.com

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

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

100%