يتيح الحدث (event) لصنف الإعلان أن شيئًا حدث دون أن يعرف من يستمع. تشترك كائنات أخرى بـ +=، وحين يطلق الصنف الحدث تُنفَّذ دالة كل مشترك.
المخرجات:
Order A-17 saved
receipt emailed for A-17
Order A-18 saved
receipt emailed for A-18
free gift added to A-18
لا يعرف Shop أن الإيصالات أو الهدايا موجودة. يطلق OrderPlaced، ويتفاعل كل من اشترك. إضافة ردّ فعل جديد (نقاط الولاء، التحليلات) تعني إضافة مشترك، لا تعديل PlaceOrder.
التصريح بحدث
تصريح الحدث عضو من نوع مفوّض مع الكلمة event:
public event EventHandler<OrderPlacedEventArgs> OrderPlaced;
يحدد نوع المفوّض توقيع المعالج. تستخدم شيفرة .NET دائمًا تقريبًا أحد نوعين مدمجين، واتباع هذا العُرف يجعل أحداثك مألوفة لكل مطوّر .NET آخر:
EventHandler:void (object sender, EventArgs e)، للأحداث التي لا تحمل بيانات. أطلقه بـEventArgs.Empty.EventHandler<TEventArgs>:void (object sender, TEventArgs e)، حيثTEventArgsصنفك الذي يحمل البيانات.
الأعراف المحيطة بهما:
senderهو الكائن الذي أطلق الحدث، وعادةthis.- يُسمّى صنف البيانات
{Something}EventArgs، ويشتق منEventArgs(اختياري منذ .NET 4.5، لكنه ما زال متّبعًا)، ويكشف خصائص للقراءة فقط. - يُسمّى الحدث بما حدث:
OrderPlacedوClosedوPriceChanged. ويُستخدم الاسم بصيغة...ing(Closing) للأحداث التي تُطلق قبل الفعل، غالبًا مع طريقة لإلغائه. - يمر الإطلاق عبر دالة
protected virtual void OnOrderPlaced(OrderPlacedEventArgs e)في الأصناف المعدّة للوراثة، كي تستطيع الأصناف المشتقة التدخل.
الاشتراك وإلغاؤه
تضيف += معالجًا وتحذفه -=. يمكن أن يكون المعالج دالة أو تعبير lambda. لحذف تعبير lambda لاحقًا احتفظ به في متغيّر، لأن كتابة تعبير lambda نفسه مجددًا تنشئ مفوّضًا مختلفًا لن تجده -=:
المخرجات:
set to 32
display shows 32 C
alarm: too hot
set to 35
display shows 35 C
set to 20
display shows 20 C
حذف معالج لم يُضف قط ليس خطأ؛ لا يفعل شيئًا. ويُظهر EventHandler<int> أن وسيط النوع لا يلزم أن يشتق من EventArgs في .NET الحديثة، مع أن الصنف المخصص يترك مجالًا لإضافة حقول لاحقًا دون كسر المشتركين.
تُنفَّذ المعالجات بشكل متزامن، بترتيب الاشتراك، على الخيط الذي أطلق الحدث. إن رمى معالج استثناءً فلا تُنفَّذ المعالجات الباقية وينتقل الاستثناء إلى الشيفرة التي أطلقت الحدث.
إطلاق حدث بأمان
الحدث بلا مشتركين يحمل null. استدعاؤه مباشرة سيرمي NullReferenceException، فأطلقه بالمعامل الشرطي لـ null:
OrderPlaced?.Invoke(this, args);
قبل C# 6 كان النمط نسخ الحقل إلى متغيّر محلي، وفحصه، واستدعاء النسخة. النسخة مهمة في الشيفرة متعددة الخيوط: فحص OrderPlaced != null ثم استدعاء OrderPlaced(...) يقرأ الحقل مرتين، وقد يحذف خيط آخر المعالج الأخير بينهما. أما ?. فتقرؤه مرة واحدة، فهي أقصر وصحيحة معًا.
لماذا event لا حقل مفوّض عام
دون الكلمة event يعمل حقل المفوّض العام للاشتراك، لكنه يسلّم كل مشترك تحكمًا كاملًا:
المخرجات:
analytics ping
analytics ping
حذفت = واحدة كُتبت بدل += المعالجين السابقين بصمت، واستطاعت الشيفرة الخارجية إطلاق "النقرة" دون أي نقرة. تعليم الحقل بـ event يجعل السطرين خطأَي ترجمة:
error CS0070: The event 'Button.Clicked' can only appear on the left hand side of += or -= (except when used from within the type 'Button')
داخل Button ما زال الحدث يتصرف كحقل مفوّض عادي، فيستطيع الصنف استدعاءه وفحصه لـ null.
تسرّب نسيان إلغاء الاشتراك
حين تشترك يخزّن الحدث مفوّضًا، ويحمل المفوّض مرجعًا إلى كائن المشترك. ما دام الناشر حيًا والمعالج مرتبطًا لا يمكن جمع المشترك كنفايات، ويظل يتلقى الأحداث بعد أن تنتهي منه:
المخرجات:
closed widget shows 101.5
open widget shows 101.5
subscribers left: 1
closed widget shows 99.0
ضبط leaky على null لم يفعل شيئًا للتغذية: ما زالت قائمة معالجاتها تشير إلى تلك الأداة، فتظل الأداة "المغلقة" تتحدّث وتبقى في الذاكرة. أما الأداة في كتلة using فألغت اشتراكها في Dispose وتوقفت عن التلقي. هذا من أشيع تسرّبات الذاكرة في شيفرة سطح المكتب والخوادم في .NET، خاصة مع الأحداث الساكنة والخدمات على مستوى التطبيق، التي تعيش حتى تنتهي العملية.
القاعدة: من يشترك في حدث على كائن يعيش أطول منه يجب أن يلغي اشتراكه، عادة في Dispose (راجع العبارة using). وحين يكون للناشر والمشترك العمر نفسه، مثل نموذج وأزراره الخاصة، لا حاجة إلى إلغاء الاشتراك.
موصّلات add وremove المخصصة
يمكن للحدث أن يحدد ما تفعله += و-=، مثل get وset في الخاصية. هذا نادر، لكنه طريقة الأطر في تخزين المعالجات في قاموس مشترك أو تمريرها إلى كائن آخر:
private EventHandler closed;
public event EventHandler Closed
{
add { Console.WriteLine("subscriber added"); closed += value; }
remove { Console.WriteLine("subscriber removed"); closed -= value; }
}
مع الموصّلات المخصصة يطلق الصنف الحدث عبر الحقل الداعم (closed?.Invoke(this, EventArgs.Empty))، لأن الحدث نفسه لم يعد له تخزين.
الأسئلة الشائعة
ما هو الحدث (event) في C#؟
الحدث عضو في صنف يتيح لكائنات أخرى طلب إعلامها حين يحدث شيء. يقوم على مفوّض متعدد، لكن الشيفرة الخارجية لا تستطيع إلا إضافة معالجات (+=) وحذفها (-=). ولا يستطيع إطلاقه إلا الصنف الذي يصرّح به.
ما الفرق بين الحدث والمفوّض في C#؟
المفوّض نوع يشير إلى دوال. أما الحدث فعضو مصرّح به بنوع مفوّض مع الكلمة event، التي تقيّد الوصول: من خارج الصنف لا تستطيع استدعاءه ولا قراءة قائمة معالجاته ولا الإسناد إليه بـ =، بل الاشتراك وإلغاؤه فقط. حقل المفوّض العام يسمح بكل ذلك، فيستطيع أي مشترك محو الآخرين أو إطلاق الحدث.
كيف أمرّر بيانات مع حدث في C#؟
اشتق صنفًا من EventArgs تكون فيه البيانات خصائص للقراءة فقط (public class OrderPlacedEventArgs : EventArgs { public decimal Total { get; } }) وصرّح بالحدث كـ EventHandler<OrderPlacedEventArgs>. أطلقه بـ OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(total));.
كيف أطلق حدثًا بأمان في C#؟
استخدم MyEvent?.Invoke(this, args);. الحدث بلا مشتركين يكون null، فاستدعاؤه مباشرة يرمي NullReferenceException. يقرأ المعامل ?. الحقل مرة واحدة، وهذا يتجنّب أيضًا حالة سباق يحذف فيها خيط آخر آخر معالج بين فحص null والاستدعاء.
هل يمكن أن تسبّب الأحداث تسرّبًا للذاكرة في C#؟
نعم. الاشتراك يخزّن مفوّضًا يشير إلى المشترك، فيبقي الناشر المشترك حيًا ما دام الناشر حيًا. الناشر طويل العمر (حدث ساكن، خدمة على مستوى التطبيق) مع مشتركين قصيري العمر لا يلغون اشتراكهم أبدًا يبقي كل واحد منهم في الذاكرة. ألغِ الاشتراك بـ -= حين ينتهي المشترك، عادة في Dispose.