event מאפשר למחלקה להכריז שמשהו קרה בלי לדעת מי מקשיב. אובייקטים אחרים נרשמים עם +=, וכשהמחלקה מפעילה את ה-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
הצהרה על event היא איבר מטיפוס delegate עם מילת המפתח event:
public event EventHandler<OrderPlacedEventArgs> OrderPlaced;
טיפוס ה-delegate קובע את החתימה של ה-handler. קוד .NET כמעט תמיד משתמש באחד משני טיפוסים מובנים, והקפדה על המוסכמה הופכת את ה-events שלכם למוכרים לכל מפתח .NET אחר:
EventHandler:void (object sender, EventArgs e), ל-events שלא נושאים נתונים. הפעילו אותו עםEventArgs.Empty.EventHandler<TEventArgs>:void (object sender, TEventArgs e), כאשרTEventArgsהיא המחלקה שלכם שנושאת את הנתונים.
המוסכמות שסביבם:
senderהוא האובייקט שהפעיל את ה-event, בדרך כללthis.- מחלקת הנתונים נקראת
{Something}EventArgs, יורשת מ-EventArgs(אופציונלי מאז .NET 4.5, אבל עדיין מקובל), וחושפת מאפיינים לקריאה בלבד. - ה-event נקרא על שם מה שקרה:
OrderPlaced,Closed,PriceChanged. שם בסגנון...ing(Closing) משמש ל-events שמופעלים לפני הפעולה, לעיתים קרובות עם דרך לבטל אותה. - ההפעלה עוברת דרך מתודה
protected virtual void OnOrderPlaced(OrderPlacedEventArgs e)במחלקות שנועדו לירושה, כדי שמחלקות יורשות יוכלו להתחבר.
הרשמה וביטול הרשמה
+= מוסיף handler ו--= מסיר אותו. handler יכול להיות מתודה או lambda. כדי להסיר lambda בהמשך, שמרו אותה במשתנה, כי כתיבה חוזרת של אותה lambda יוצרת delegate אחר ש--= לא ימצא:
פלט:
set to 32
display shows 32 C
alarm: too hot
set to 35
display shows 35 C
set to 20
display shows 20 C
הסרה של handler שאף פעם לא נוסף אינה שגיאה; היא לא עושה כלום. EventHandler<int> מראה שארגומנט הטיפוס לא חייב לרשת מ-EventArgs ב-.NET מודרני, אם כי מחלקה ייעודית משאירה מקום להוסיף שדות בהמשך בלי לשבור מנויים.
handlers רצים באופן סינכרוני, לפי סדר ההרשמה, על ה-thread שהפעיל את ה-event. אם handler אחד זורק, ה-handlers הנותרים לא רצים והחריגה מתפשטת לקוד שהפעיל את ה-event.
הפעלה בטוחה של event
event בלי מנויים מחזיק null. קריאה ישירה אליו הייתה זורקת NullReferenceException, ולכן מפעילים אותו עם האופרטור null-conditional:
OrderPlaced?.Invoke(this, args);
לפני C# 6 התבנית הייתה להעתיק את השדה למשתנה מקומי, לבדוק אותו, ולהפעיל את העותק. העותק חשוב בקוד מרובה threads: בדיקה של OrderPlaced != null ואז קריאה ל-OrderPlaced(...) קוראות את השדה פעמיים, ו-thread אחר יכול להסיר את ה-handler האחרון ביניהן. ?. קורא אותו פעם אחת, ולכן הוא גם קצר יותר וגם נכון.
למה event ולא שדה delegate ציבורי
בלי מילת המפתח event, שדה delegate ציבורי עובד להרשמה, אבל הוא נותן לכל מנוי שליטה מלאה:
פלט:
analytics ping
analytics ping
= אחד שהוקלד במקום += מחק בשקט את שני ה-handlers הקודמים, וקוד חיצוני יכול היה להפעיל את ה"לחיצה" בלי שום לחיצה. סימון השדה כ-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, ה-event עדיין מתנהג כמו שדה delegate רגיל, כך שהמחלקה יכולה להפעיל אותו ולבדוק אם הוא null.
הדליפה של ביטול הרשמה שנשכח
כשנרשמים, ה-event שומר delegate, וה-delegate מחזיק הפניה לאובייקט המנוי. כל עוד המפרסם חי וה-handler מחובר, ה-garbage collector לא יכול לאסוף את המנוי, והוא ממשיך לקבל events גם אחרי שסיימתם איתו:
פלט:
closed widget shows 101.5
open widget shows 101.5
subscribers left: 1
closed widget shows 99.0
הצבת null ב-leaky לא עשתה כלום מבחינת ה-feed: רשימת ה-handlers שלו עדיין מפנה לווידג'ט הזה, כך שהווידג'ט ה"סגור" ממשיך להתעדכן ונשאר בזיכרון. הווידג'ט שבבלוק ה-using ביטל הרשמה ב-Dispose והפסיק לקבל. זו אחת מדליפות הזיכרון הנפוצות ביותר בקוד .NET של דסקטופ ושרתים, במיוחד עם events סטטיים ושירותים ברמת האפליקציה, שחיים עד שהתהליך מסתיים.
הכלל: מי שנרשם ל-event על אובייקט שחי יותר זמן ממנו חייב לבטל את ההרשמה, בדרך כלל ב-Dispose (ראו פקודת using). כשלמפרסם ולמנוי יש אותו אורך חיים, כמו טופס והכפתורים שלו עצמו, אין צורך לבטל הרשמה.
accessors מותאמים של add ו-remove
event יכול להגדיר מה += ו--= עושים, כמו get ו-set של מאפיין. זה נדיר, אבל כך frameworks שומרים handlers במילון משותף או מעבירים אותם לאובייקט אחר:
private EventHandler closed;
public event EventHandler Closed
{
add { Console.WriteLine("subscriber added"); closed += value; }
remove { Console.WriteLine("subscriber removed"); closed -= value; }
}
עם accessors מותאמים, המחלקה מפעילה את ה-event דרך שדה הגיבוי (closed?.Invoke(this, EventArgs.Empty)), כי ל-event עצמו כבר אין אחסון.
שאלות נפוצות
מהו event ב-C#?
event הוא איבר של מחלקה שמאפשר לאובייקטים אחרים לבקש לקבל הודעה כשמשהו קורה. מאחוריו עומד multicast delegate, אבל קוד חיצוני יכול רק להוסיף handlers (+=) ולהסיר אותם (-=). רק המחלקה שמצהירה על ה-event יכולה להפעיל אותו.
מה ההבדל בין event ל-delegate ב-C#?
delegate הוא טיפוס שמפנה למתודות. event הוא איבר שמוצהר עם טיפוס delegate ועם מילת המפתח event, שמגבילה את הגישה: מחוץ למחלקה אי אפשר להפעיל אותו, לקרוא את רשימת ה-handlers שלו או להציב לו עם =, רק להירשם ולבטל הרשמה. שדה delegate ציבורי מאפשר את כל זה, כך שכל מנוי יכול למחוק את האחרים או להפעיל את ה-event.
איך מעבירים נתונים עם event ב-C#?
גזרו מחלקה מ-EventArgs עם הנתונים כמאפיינים לקריאה בלבד (public class OrderPlacedEventArgs : EventArgs { public decimal Total { get; } }) והצהירו על ה-event כ-EventHandler<OrderPlacedEventArgs>. הפעילו אותו עם OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(total));.
איך מפעילים event בבטחה ב-C#?
השתמשו ב-MyEvent?.Invoke(this, args);. event בלי מנויים הוא null, כך שקריאה ישירה אליו זורקת NullReferenceException. האופרטור ?. קורא את השדה פעם אחת, וזה גם מונע race שבו thread אחר מסיר את ה-handler האחרון בין בדיקת ה-null לקריאה.
האם events ב-C# יכולים לגרום לדליפות זיכרון?
כן. הרשמה שומרת delegate שמפנה למנוי, כך שהמפרסם שומר את המנוי בחיים כל עוד המפרסם חי. מפרסם ארוך חיים (event סטטי, שירות ברמת האפליקציה) עם מנויים קצרי חיים שאף פעם לא מבטלים הרשמה שומר כל אחד מהם בזיכרון. בטלו הרשמה עם -= כשהמנוי סיים, בדרך כלל ב-Dispose.