C#: المندوبون والفعاليات

1. مفاهيم التفويض

المندوب (Delegate) هو مؤشر دالة آمن من حيث النوع في لغة C#، يتيح تمرير الطرق كمعلمات. يحدد المندوب توقيع الطريقة (نوع القيمة المرجعة وقائمة المعلمات)، ولا يمكن تعيين سوى الطرق التي تتطابق مع هذا التوقيع إلى مثيل المندوب.

على عكس مؤشرات الدوال في لغتي C وC++، فإن المندوبين (delegates) موجهة للكائنات، وتتميز بالأمان من حيث النوع، وتدعم الاستدعاء المتعدد.


2. تعريف المندوبين واستخدامهم

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

▶ مثال

CSHARP
delegate int MathOp(int a, int b);

class Program
{
    static int Add(int a, int b) => a + b;
    static int Multiply(int a, int b) => a * b;

    static void Main()
    {
        MathOp op = Add;
        Console.WriteLine(op(3, 4));

        op = Multiply;
        Console.WriteLine(op(3, 4));
    }
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
7
12

3. مندوبو البث المتعدد

تدعم المندوبات ميزة البث المتعدد، مما يعني أن مثيلًا واحدًا من المندوبة يمكنه الإشارة إلى عدة طرق. استخدم += لإضافة طريقة و-= لإزالة واحدة. عند الاستدعاء، يتم تنفيذ جميع الطرق بالترتيب الذي أُضيفت به. بالنسبة للمندوبين الذين يعيدون قيمًا، فإن الاستدعاء المتعدد لا يعيد سوى نتيجة الطريقة الأخيرة.

▶ مثال

CSHARP
delegate void NotifyHandler(string msg);

class Program
{
    static void LogToConsole(string msg) => Console.WriteLine($"Console: {msg}");
    static void LogToFile(string msg) => Console.WriteLine($"File: {msg}");
    static void LogToDb(string msg) => Console.WriteLine($"Database: {msg}");

    static void Main()
    {
        NotifyHandler handler = LogToConsole;
        handler += LogToFile;
        handler += LogToDb;

        handler("System started");

        Console.WriteLine("---");

        handler -= LogToFile;
        handler("System shutdown");
    }
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Console: System started
File: System started
Database: System started
---
Console: System shutdown
Database: System shutdown

4. المندوبون المدمجون

توفر لغة C# ثلاثة مندوبين مدمجين شائعي الاستخدام يغطون معظم السيناريوهات دون الحاجة إلى تعريفات مخصصة.

(1) الإجراء

Action تمثل دالة لا تُرجع أي قيمة. وهي تدعم ما بين 0 و16 معلمة عامة: Action، Action<T>، Action<T1,T2>، إلخ.

▶ مثال

CSHARP
class Program
{
    static void Main()
    {
        Action```<string>``` greet = name => Console.WriteLine($"Hello, {name}!");
        greet("Alice");

        Action noParam = () => Console.WriteLine("Parameterless Action");
        noParam();
    }
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Hello, Alice!
Parameterless Action

(2) Func

Func<T, TResult> تمثل دالة ذات قيمة مرتجعة. المعلمة العامة الأخيرة هي نوع القيمة المرتجعة، أما المعلمات التي تسبقها فهي معلمات الإدخال: Func<TResult>، Func<T, TResult>، Func<T1,T2,TResult>، إلخ.

▶ مثال

CSHARP
class Program
{
    static void Main()
    {
        Func`<int, int, int>` add = (a, b) => a + b;
        Console.WriteLine(add(10, 20));

        Func```<string>``` getTime = () => DateTime.Now.ToString("HH:mm:ss");
        Console.WriteLine(getTime());
    }
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
30
14:30:00

(3) المسند

Predicate<T> تمثل دالة تُرجع bool، وتُستخدم عادةً في عمليات التصفية والفحوصات الشرطية.

▶ مثال

CSHARP
class Program
{
    static void Main()
    {
        Predicate```<int>``` isEven = n => n % 2 == 0;
        Console.WriteLine(isEven(4));
        Console.WriteLine(isEven(7));

        var numbers = new List```<int>``` { 1, 2, 3, 4, 5, 6 };
        var evens = numbers.FindAll(isEven);
        Console.WriteLine(string.Join(", ", evens));
    }
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
True
False
2, 4, 6

5. الطرق المجهولة

تستخدم الطرق المجهولة الكلمة الرئيسية delegate لتعريف كتلة تعليمات برمجية مضمنة دون الحاجة إلى طريقة مسماة منفصلة.

▶ مثال

CSHARP
class Program
{
    static void Main()
    {
        Func`<int, int>` square = delegate(int x) { return x * x; };
        Console.WriteLine(square(5));

        Action```<string>``` log = delegate(string msg) { Console.WriteLine($"[LOG] {msg}"); };
        log("Anonymous method example");
    }
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
25
[LOG] Anonymous method example
💡 تُعدُّ الطرق المجهولة السلف لتعبيرات لامدا. ويفضل مطورو لغة C# المعاصرون استخدام تعبيرات لامدا x => x * x نظرًا لصيغتها الأكثر إيجازًا.


6. الفعاليات

الحدث هو آلية نشر-اشتراك تعتمد على المندوبين. عندما يتم تعديل حقل المفوض باستخدام الكلمة الرئيسية event، لا يمكن إلا للفئة التي تعلن عن الحدث أن تثيره داخليًا؛ أما الفئات الخارجية فلا يمكنها سوى الاشتراك أو إلغاء الاشتراك باستخدام += و-=، ولا يمكنها استدعاء الحدث مباشرةً.

▶ مثال

CSHARP
class Button
{
    public event EventHandler```<string>``` OnClick;

    public void Click()
    {
        OnClick?.Invoke(this, "Button clicked");
    }
}

class Program
{
    static void Main()
    {
        var btn = new Button();
        btn.OnClick += (sender, msg) => Console.WriteLine($"Event received: {msg}");
        btn.Click();
        btn.Click();
    }
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Event received: Button clicked
Event received: Button clicked
⚠️ في حالة عدم وجود مشتركين، سيؤدي استدعاء OnClick.Invoke() مباشرةً إلى ظهور استثناء NullReferenceException، لذا يجب عليك استخدام الاستدعاء الشرطي الفارغ ?.Invoke().


7. نمط النشر والاشتراك

يُعد نمط النشر والاشتراك (الناشر والمشترك) السيناريو الأساسي لاستخدام الأحداث. يتولى الناشر مسؤولية إصدار الأحداث، بينما يتولى المشتركون مسؤولية الاستجابة لها — وهما منفصلان تمامًا عن بعضهما.

▶ مثال

CSHARP 📖 للعرض فقط
class NewsPublisher
{
    public event EventHandler```<string>``` OnNewsPublished;

    public void Publish(string news)
    {
        Console.WriteLine($"Publishing news: {news}");
        OnNewsPublished?.Invoke(this, news);
    }
}

class NewsSubscriber
{
    public string Name { get; }

    public NewsSubscriber(string name)
    {
        Name = name;
    }

    public void Subscribe(NewsPublisher publisher)
    {
        publisher.OnNewsPublished += HandleNews;
    }

    public void Unsubscribe(NewsPublisher publisher)
    {
        publisher.OnNewsPublished -= HandleNews;
    }

    private void HandleNews(object sender, string news)
    {
        Console.WriteLine($"  [{Name}] Received: {news}");
    }
}

class Program
{
    static void Main()
    {
        var publisher = new NewsPublisher();
        var sub1 = new NewsSubscriber("Reader A");
        var sub2 = new NewsSubscriber("Reader B");

        sub1.Subscribe(publisher);
        sub2.Subscribe(publisher);

        publisher.Publish("C# 12 officially released");

        sub2.Unsubscribe(publisher);
        publisher.Publish(".NET 9 performance improved by 50%");
    }
}
43 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)
TEXT 📖 للعرض فقط
Publishing news: C# 12 officially released
  [Reader A] Received: C# 12 officially released
  [Reader B] Received: C# 12 officially released
Publishing news: .NET 9 performance improved by 50%
  [Reader A] Received: .NET 9 performance improved by 50%

8. كائنات EventArgs المخصصة

عادةً ما يرث T الموجود في EventHandler<T> خصائصه من EventArgs، ويُستخدم لحمل بيانات الأحداث المخصصة.

▶ مثال

CSHARP 📖 للعرض فقط
class TemperatureEventArgs : EventArgs
{
    public double Temperature { get; }
    public DateTime Timestamp { get; }

    public TemperatureEventArgs(double temperature)
    {
        Temperature = temperature;
        Timestamp = DateTime.Now;
    }
}

class TemperatureMonitor
{
    public event EventHandler<TemperatureEventArgs> OnTemperatureAlert;
    private double _threshold;

    public TemperatureMonitor(double threshold)
    {
        _threshold = threshold;
    }

    public void CheckTemperature(double current)
    {
        Console.WriteLine($"Current temperature: {current}°C");
        if (current > _threshold)
        {
            var args = new TemperatureEventArgs(current);
            OnTemperatureAlert?.Invoke(this, args);
        }
    }
}

class Cooler
{
    public void StartCooling(object sender, TemperatureEventArgs e)
    {
        Console.WriteLine($"  🚨 Temperature exceeded! {e.Temperature}°C > threshold, starting cooling (time: {e.Timestamp:HH:mm:ss})");
    }
}

class Program
{
    static void Main()
    {
        var monitor = new TemperatureMonitor(30.0);
        var cooler = new Cooler();

        monitor.OnTemperatureAlert += cooler.StartCooling;

        monitor.CheckTemperature(25.0);
        monitor.CheckTemperature(32.5);
        monitor.CheckTemperature(28.0);
        monitor.CheckTemperature(35.0);
    }
}
48 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)
TEXT 📖 للعرض فقط
Current temperature: 25°C
Current temperature: 32.5°C
  🚨 Temperature exceeded! 32.5°C > threshold, starting cooling (time: 14:30:00)
Current temperature: 28°C
Current temperature: 35°C
  🚨 Temperature exceeded! 35°C > threshold, starting cooling (time: 14:30:01)

9. المندوبون مقابل الأحداث

مقال خاص مندوب حدث
النوع النوع (مؤشر الدالة) المُعدِّل (مثيل مندوب مقيد)
استدعاء خارجي يمكن استدعاؤه في أي مكان لا يمكن إثارة الاستثناء داخليًّا إلا من قبل الفئة المُعلِنة
تعيين خارجي يمكن استبداله بـ = لا يمكن استخدام سوى += / -=
الاستخدام وظائف الاستدعاء، نمط الاستراتيجية النشر والاشتراك، آليات الإشعار
الأمان أدنى أعلى، يشمل أذونات الاستدعاء
📌 الحدث هو في الأساس مندوب تم تطبيق المُعدِّل event عليه، وهو ما يقيد الوصول الخارجي إلى الاستدعاء والتعيين، مما يضمن أن الناشر هو الوحيد الذي يمكنه إثارة الحدث.


❓ أسئلة شائعة

س ما الفرق بين المندوب والحدث؟
ج الحدث هو مندوب تم تطبيق المُعدِّل event عليه؛ ولا يمكن للكود الخارجي الاشتراك فيه أو إلغاء الاشتراك منه إلا باستخدام +=/-=، ولا يمكنه استدعاءه أو الكتابة فوقه مباشرةً، ولا يمكن إلا للفئة المُعلِنة له أن تثيره داخليًّا.
س ما الذي تعيده دالة التفويض متعددة البث التي تحتوي على قيمة إرجاع؟
ج تعيد قيمة الإرجاع الخاصة بالطريقة التي تم استدعاؤها أخيرًا؛ أما قيم الإرجاع الخاصة بالطرق السابقة فيتم تجاهلها.
س ما الفرق بين Action و Func؟
ج لا تحتوي Action على قيمة إرجاع (void)، بينما تحتوي Func على قيمة إرجاع — حيث يمثل المعلمة العامة الأخيرة نوع القيمة المرجعة.
س لماذا يُستخدم ?.Invoke() لإطلاق حدث؟
ج لمنع حدوث NullReferenceException عندما لا يكون للحدث أي مشتركين؛ حيث يتخطى ?.Invoke() الاستدعاء تلقائيًّا عندما يكون الحدث فارغًا.
س هل «Predicate» و«Func<T, bool>» متكافئان؟
ج إنهما متكافئان من الناحية الوظيفية. Predicate<T> هو نسخة مخصصة من Func<T, bool> ذات دلالات أكثر وضوحًا، وتُستخدم عادةً في سيناريوهات التصفية.

📖 ملخص

📝 تمارين

  1. قم بتعريف مندوب delegate string Transform(string s);، واربطه بثلاث طرق لتحويل السلسلة إلى أحرف كبيرة، وتحويلها إلى أحرف صغيرة، وعكسها، ثم اختبر استدعاء البث المتعدد
  2. استخدم Func<int, int, bool> لتنفيذ مُقارن يحدد ما إذا كان الرقم الأول أكبر من الرقم الثاني
  3. إنشاء فئة FileWatcher تحتوي على event EventHandler``<string>`` OnFileChanged، لمحاكاة إرسال الإشعارات إلى المشتركين عند حدوث تغيير في الملف
  4. تعريف متغير مخصص StockPriceEventArgs (يحتوي على رمز السهم والسعر ونسبة التغير)، وتطبيق أداة مراقبة أسعار الأسهم التي تطلق حدثًا عندما تتجاوز نسبة التغير عتبة معينة
  5. قارن بين استخدام مندوب مخصص وEventHandler<T> لتنفيذ الحدث نفسه، ولخص السيناريوهات التي يمكن تطبيق كل منهما فيها
Web-Tutorial.com

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

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

100%