C#: المندوبون والفعاليات
1. مفاهيم التفويض
المندوب (Delegate) هو مؤشر دالة آمن من حيث النوع في لغة C#، يتيح تمرير الطرق كمعلمات. يحدد المندوب توقيع الطريقة (نوع القيمة المرجعة وقائمة المعلمات)، ولا يمكن تعيين سوى الطرق التي تتطابق مع هذا التوقيع إلى مثيل المندوب.
على عكس مؤشرات الدوال في لغتي C وC++، فإن المندوبين (delegates) موجهة للكائنات، وتتميز بالأمان من حيث النوع، وتدعم الاستدعاء المتعدد.
2. تعريف المندوبين واستخدامهم
استخدم الكلمة الرئيسية delegate لإعلان نوع مندوب، ثم أنشئ مثيلًا واربطه بأحد الأساليب.
▶ مثال
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));
}
}
7
12
3. مندوبو البث المتعدد
تدعم المندوبات ميزة البث المتعدد، مما يعني أن مثيلًا واحدًا من المندوبة يمكنه الإشارة إلى عدة طرق. استخدم += لإضافة طريقة و-= لإزالة واحدة. عند الاستدعاء، يتم تنفيذ جميع الطرق بالترتيب الذي أُضيفت به. بالنسبة للمندوبين الذين يعيدون قيمًا، فإن الاستدعاء المتعدد لا يعيد سوى نتيجة الطريقة الأخيرة.
▶ مثال
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");
}
}
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>، إلخ.
▶ مثال
class Program
{
static void Main()
{
Action```<string>``` greet = name => Console.WriteLine($"Hello, {name}!");
greet("Alice");
Action noParam = () => Console.WriteLine("Parameterless Action");
noParam();
}
}
Hello, Alice!
Parameterless Action
(2) Func
Func<T, TResult> تمثل دالة ذات قيمة مرتجعة. المعلمة العامة الأخيرة هي نوع القيمة المرتجعة، أما المعلمات التي تسبقها فهي معلمات الإدخال: Func<TResult>، Func<T, TResult>، Func<T1,T2,TResult>، إلخ.
▶ مثال
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());
}
}
30
14:30:00
(3) المسند
Predicate<T> تمثل دالة تُرجع bool، وتُستخدم عادةً في عمليات التصفية والفحوصات الشرطية.
▶ مثال
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));
}
}
True
False
2, 4, 6
5. الطرق المجهولة
تستخدم الطرق المجهولة الكلمة الرئيسية delegate لتعريف كتلة تعليمات برمجية مضمنة دون الحاجة إلى طريقة مسماة منفصلة.
▶ مثال
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");
}
}
25
[LOG] Anonymous method example
x => x * x نظرًا لصيغتها الأكثر إيجازًا.
6. الفعاليات
الحدث هو آلية نشر-اشتراك تعتمد على المندوبين. عندما يتم تعديل حقل المفوض باستخدام الكلمة الرئيسية event، لا يمكن إلا للفئة التي تعلن عن الحدث أن تثيره داخليًا؛ أما الفئات الخارجية فلا يمكنها سوى الاشتراك أو إلغاء الاشتراك باستخدام += و-=، ولا يمكنها استدعاء الحدث مباشرةً.
▶ مثال
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();
}
}
Event received: Button clicked
Event received: Button clicked
OnClick.Invoke() مباشرةً إلى ظهور استثناء NullReferenceException، لذا يجب عليك استخدام الاستدعاء الشرطي الفارغ ?.Invoke().
7. نمط النشر والاشتراك
يُعد نمط النشر والاشتراك (الناشر والمشترك) السيناريو الأساسي لاستخدام الأحداث. يتولى الناشر مسؤولية إصدار الأحداث، بينما يتولى المشتركون مسؤولية الاستجابة لها — وهما منفصلان تمامًا عن بعضهما.
▶ مثال
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%");
}
}
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، ويُستخدم لحمل بيانات الأحداث المخصصة.
▶ مثال
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);
}
}
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 عليه؛ ولا يمكن للكود الخارجي الاشتراك فيه أو إلغاء الاشتراك منه إلا باستخدام +=/-=، ولا يمكنه استدعاءه أو الكتابة فوقه مباشرةً، ولا يمكن إلا للفئة المُعلِنة له أن تثيره داخليًّا.?.Invoke() لإطلاق حدث؟NullReferenceException عندما لا يكون للحدث أي مشتركين؛ حيث يتخطى ?.Invoke() الاستدعاء تلقائيًّا عندما يكون الحدث فارغًا.<T, bool>» متكافئان؟Predicate<T> هو نسخة مخصصة من Func<T, bool> ذات دلالات أكثر وضوحًا، وتُستخدم عادةً في سيناريوهات التصفية.📖 ملخص
- المندوبون هم مؤشرات دالة آمنة من حيث النوع تُحدد توقيعات الطرق
- استخدم الكلمة الرئيسية
delegateلإعلان أنواع المندوبين المخصصة - تجمع الدلائل متعددة الإرسال بين عدة طرق عبر
+=/-=، وتقوم بتنفيذها بالترتيب Actionلا تُرجع أي قيمة، وFuncتُرجع قيمة، وPredicateتُرجع قيمة من نوع bool- تستخدم الطرق المجهولة
delegate{ }للتعريف المضمن؛ وتُعد تعبيرات لامدا بديلاً أكثر إيجازًا - تقيّد الكلمة الرئيسية
eventأذونات استدعاء المندوب وتعيينه، بحيث لا تسمح إلا للفئة المُعلِنة بإثارة هذا المندوب - يعمل نمط النشر والاشتراك على فصل الناشرين عن المشتركين
- ترث كائنات EventArgs المخصصة من الفئة الأساسية EventArgs لنقل بيانات الحدث
📝 تمارين
- قم بتعريف مندوب
delegate string Transform(string s);، واربطه بثلاث طرق لتحويل السلسلة إلى أحرف كبيرة، وتحويلها إلى أحرف صغيرة، وعكسها، ثم اختبر استدعاء البث المتعدد - استخدم
Func<int, int, bool>لتنفيذ مُقارن يحدد ما إذا كان الرقم الأول أكبر من الرقم الثاني - إنشاء فئة
FileWatcherتحتوي علىevent EventHandler``<string>`` OnFileChanged، لمحاكاة إرسال الإشعارات إلى المشتركين عند حدوث تغيير في الملف - تعريف متغير مخصص
StockPriceEventArgs(يحتوي على رمز السهم والسعر ونسبة التغير)، وتطبيق أداة مراقبة أسعار الأسهم التي تطلق حدثًا عندما تتجاوز نسبة التغير عتبة معينة - قارن بين استخدام مندوب مخصص و
EventHandler<T>لتنفيذ الحدث نفسه، ولخص السيناريوهات التي يمكن تطبيق كل منهما فيها