כלומר לא מחזירים ערך ומקבלים object וטיפוס שיורש מEventArgs.
בFramework אכן ניצלו עובדה זו ויצרו EventHandler גנרי:
1
publicdelegatevoid EventHandler<TEventArgs>(object sender, TEventArgs e) where TEventArgs : EventArgs;
בMSDN ממליצים להשתמש בDelegate זה לשימוש בEventים שלנו.
למען האמת, מיקרוסופט באמת התמידה להשתמש בDelegate זה בקוד שפותח אחרי Framework 2.0 ואכן אם נסתכל על Eventים מWCF או WorkFlow הם באמת משתמשים בטיפוס EventHandler<TEventArgs>.
משום מה בטכנולוגיה WPF שיצאה כבר בFramework 3.0 (סמוך לWCF), אין שימוש בDelegate זה עבור אירועים, אלא משתמשים בDelegate מיוחד עבור כל סוג אירוע…
קימפלתי וראיתי שהכל מתקמפל. אחר כך שמתי לב שקרה פה משהו מוזר – כל הOverloadים של הConstructorים של XElement וXAttribute מקבלים משהו שנקרא XName.
אלא שבאחת הקריאות (הקריאה האחת לפני האחרונה) לא העברתי לConstructor את הטיפוס הזה, אלא מחרוזת רגילה, כי שכחתי לקרוא לXName.Get.
מה שמדהים בכל הסיפור הזה זה שזה מתקמפל.
התחלתי לחקור את הסיפור:
אולי יש Constructor שלא ראיתי? חיפשתי וחיפשתי ולא מצאתי. אז אולי יש Extension method שיכול לקבל string במקום לקבל XName?
לא יכול להיות דבר כזה, כי אין Extension methods לConstructorים..
בקיצור, המשכתי לחפש. אולי אני טועה, אולי התוכנה לא באמת מתקמפלת? אלא שבדקתי והתוכנה כן מתקמפלת.
מה שהיה ממש מוזר בכל הסיפור הזה, זה שכשעשיתי Go to definition לConstructor הזה, הוא הביא אותי לConstructor שמצפה לקבל XName, כלומר לאותו Constructor שביצעתי קריאה אליו בשורות הקודמות.
אחרי כמה זמן של חיפושים ומחשבות הגעתי לתשובה – קיים Implicit cast בין XName לstring!
אחרי שגיליתי שהוא קיים, אני מאוד שמח שהוא קיים, כיוון שהוא חוסך כתיבה. אלא שהיה נחמד אם היה יותר קל לזהות את זה.
שימו לב שאם אתם כותבים implicit casts משלכם, שזה לא גלוי לעין שיש הסבה כזאת.
כשקוראים קוד שמשתמש בהסבות כאלה, לא רואים הכל, ועד לדיבוג אפשר לחשוב שמבינים את כל הקוד, אבל בעצם מתבצעת פה קריאה לפונקציה, באופן נסתר מהעין. כדאי לשקול זאת בשימוש.
יש כמה אופרטורים מיוחדים שלא כל כך טריוואלי שאפשר לעשות להם Operator overload.
באחד מהם נפגשנו בעבר (טיפ מספר 2).
אחד האופרטורים הנוספים הוא אופרטור ששמו implicit. זה נראה כמו משהו חדש, אבל בעצם כולנו כבר מכירים אותו:
נניח שיש לנו את הקוד הבא:
1
2
int seven = 7;
float number = seven; // 7.0
מה בדיוק קורה כאן? ובכן, אם נזכר בטיפ על ההבדל בין Value types וReference types נשים לב שבValue types כמו int וfloat אין ירושה.
לכן מה שקורה כאן זו לא "סתם" המרה ממחלקת בן למחלקת אב, אלא ממש איזשהי המרה המאפשרת לנו להסב את המשתנה הראשונה לסוג של המשתנה השני באופן לא טבעי.
מאחר והסבה זו אינה מאבדת מידע, היא נעשית באופן "שקוף", כלומר implicitly. אם ההסבה הייתה מאבדת מידע (למשל מfloat לint), אז היא לא הייתה שקופה, והיינו צריכים לבצע אותה באופן מפורש.
זה מתבצע באמצעות קריאה של הקומפיילר לפונקציה מתאימה המבצעת הסבה זו.
אנחנו יכולים לעשות דברים כאלה בעצמנו: נניח שיש לנו מחלקה המייצגת מעלות ומחלקה המייצגת רדיאנים:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
publicclassRadians
{
publicdouble Angle
{
get;
privateset;
}
publicRadians(double angle)
{
this.Angle = angle;
}
}
publicclassDegrees
{
publicdouble Angle
{
get;
privateset;
}
publicDegrees(double angle)
{
this.Angle = angle;
}
}
נוכל לכתוב הסבה שהיא implicit ביניהם. נכתוב למשל בתוך המחלקה של הרדיאנים:
שימו לב שזה גם מסוכן, כיוון שאנחנו יכולים לכתוב קוד שלא התכוונו לכתוב ומתקמפל, למשל להחזיר בטעות מפונקציה שאמורה להחזיר מעלות, רדיאנים.
למשל נניח שיש לנו פונקציה שאמורה להחזיר את סכום הזוויות במשולש. נניח שבטעות רשמנו משהו כזה:
1
2
3
4
publicstatic Degrees SumOfTriangleAngles()
{
returnnew Radians(180);
}
הדבר מתקמפל, אבל לא זאת הייתה הכוונה שלנו, כיוון שעכשיו יוחזר לנו מהפונקציה 180 רדיאנים במעלות, במקום 180 מעלות.
מתי בכל זאת כדאי להשתמש?
כשאתם רוצים לעשות ששני אובייקטים יהיו "שקולים" בלי לרשת אחד מהשני.
למשל, נניח שיש לכם אובייקט משלכם שמייצג שגיאה שאפשר גם לכתוב לLog ולעשות איתו עוד דברים, אז אפשר לעשות הסבה implicit מException לאובייקט שלכם.
עוד דוגמה היא כזאת – נניח שיש לכם אובייקט שמייצג כתובת ונניח שיש לו ייצוג מחרוזתי לכתובת זו (למשל בעזרת הפונקציה ToString). אפשר ליצור הסבה implicit בין המחרוזת המייצגת את האובייקט לאובייקט שלכם. למשל:
1
IPAddress localHost = "127.0.0.1";
בכל אופן, חשוב לדאוג שההסבה לא תאבד מידע, מאחר והיא מתבצעת באופן בלתי גלוי לעין.
שימו לב שלא נוכל ליצור הסבות ממחלקת אב או הסבות למחלקות אב – אלו שמורות ע"י הקומפיילר.
מדי פעם היינו רוצים לכתוב אופרטורים משלנו למחלקות.
למשל, נניח שאנחנו כותבים מחלקה המייצגת מבנה מתמטי עם חיבור או כפל, למשל מטריצות 5x5, מספרים רציונליים או מספרים מרוכבים, אז היינו רוצים שיהיה אפשר לחבר שני איברים מהמבנה הזה עם אופרטור ה+, כמו שאפשר לחבר שני intים.
ניתן לעשות זאת באמצעות משהו שנקרא Operator overload. מה זה? מדובר בעצם פונקציה סטטית שתפעל ברגע שיפעילו את האופרטור שלנו.
זהו, ככה אפשר להשתמש בפונקציות האלה בצורה יותר נוחה:
1
2
3
4
Matrix5x5 firstMatrix;
Matrix5x5 secondMatrix;
Matrix5x5 result = firstMatrix + secondMatrix;
Matrix5x5 multipliedByScalar = 9 * firstMatrix;
יש פה מגבלה שהיא די מובנת – האופרטור חייב לקבל בתור אחד הפרמטרים את הטיפוס של המחלקה, אחרת היינו יכולים לעשות Overloadים לטיפוסים שהם לא שלנו, והדבר לא רצוי.
יש לזה חסרון שהוא לא מובן ממבט ראשון – אי אפשר ליצור Operator Overload לממשקים, או בכלל להכריח ממשק שיהיה לו אופרטור כזה (למשל, היינו רוצים שלממשק שמייצג מספר כלשהו, יהיה גם אופרטור חיבור).
כשראיתי את הקוד הזה לראשונה לא כל כך הבנתי מה קורה כאן.
בכל מקרה התשובה היא שהדבר נעשה משיקולי Thread-safety. כאשר אנחנו מוסיפים או מבטלים הרשמה לEvent, לא מדובר בפעולה אטומית ולכן אם היא תקרה בו זמנית משני Threadים, היא עשויה להסתכם בכך שאחד משני הרישומים לא התבצע.
כדי למנוע זאת, הקומפיילר מייצר קוד כזה מאחורי הקלעים. מה שקורה כאן זה שאנחנו מנסים לעשות Delegate.Combine לDelegate שקיבלנו (שמו כזכור value), זה יוצר Delegate חדש שהוא שרשור של שני הDelegateים.
אחרי שעשינו את זה, אנחנו מכניסים את התוצאה ל handler3. אחרי זה אנחנו קוראים לInterlocked.CompareExchange – זו פונקציה שדומה לקוד הבא:
1
2
3
4
5
6
raised = mRaised;
if (mRaised == handler2)
{
mRaised = handler3;
}
כלומר מחליף את mRaised בDelegate המשורשר בתנאי שmRaised עדיין שווה לhandler2 ומכניס לraised את הערך המקורי שלmRaised. ההבדל המשמעותי בין הקוד שרשמתי פה לשימוש בInterlocked.CompareExchange, זה שInterlocked.CompareExchange הוא אטומי. כלומר, 3 השורות שציינתי מתבצעות בפעולת מעבד אחת, ולכן זה Thread Safe.
כעת הדבר חוזר על עצמו כל עוד raised לא שווה לhandler2.
אנחנו נצא מהלולאה כאשר raised יהיה שווה לhandler2, כלומר כאשר mRaised לפני השרשור של הEvent שווה לmRaised אחרי שרשור הEvent. כלומר כאשר לא התערב לנו Thread אחר בערך של mRaised.
זהו, בסה"כ זה די מפחיד, אבל אפשר להבין מה קורה כאן.
כמובן, אנחנו יכולים כמו פעם קודמת לממש את add וremove בדרך המסורתית במידה ואנחנו לא אוהבים את מה שקורה כאן.
ראינו שזה בעצם Delegate שהקומפיילר מגביל את השימוש שלו מחוץ למחלקה בה הוא נמצא, כך שיהיה אפשר רק להירשם אליו או להסיר רישום ממנו.
אמרתי גם שזה מזכיר קצת את ההכמסה שמבצע Property.
אך אפשר לומר שProperty נותן יותר גמישות, מאחר ואנחנו יכולים לכתוב איזה קוד שאנחנו רוצים בaccessorים שלו (הgetter או הsetter), ואילו Event זו מגביל לשימוש בadd וremove, אבל בלי כתיבת קוד בגישות אלה.
אלא שקיימת אפשרות גם לכתוב לוגיקה בהוספה והסרה של eventים.
זה מתבצע באמצעות הכתיבה הבאה:
במקום כך:
1
2
3
4
publicclassEventRaiser
{
publicevent EventHandler Raised;
}
נכתוב כך:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
publicclassEventRaiser
{
private EventHandler mRaised;
publicevent EventHandler Raised
{
add
{
mRaised += value;
}
remove
{
mRaised -= value;
}
}
}
זו כתיבה שמאוד דומה לכתיבה של Properties. כאשר תתבצע הרשמה או הסרת רישום לאירוע, יקראו בהתאמה הAccessorים של הadd או הremove.
כך שאם אנחנו רוצים לעשות לוגיקה משלנו, אנחנו יכולים לעשות אותה בAccessorים אלה:
לדוגמה,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
privateint mNumOfSubscriptions;
publicevent EventHandler Raised
{
add
{
mRaised += value;
mNumOfSubscriptions++;
}
remove
{
mRaised -= value;
// TODO: Check if the handler was already registered
// TODO: and decrease the number of subscriptions.
mNumOfSubscriptions--;
}
}
למעשה, פונקציות אלה נוצרות מאחורי הקלעים גם אם השתמשנו בSyntax הראשון:
1
2
3
4
publicclassEventRaiser
{
publicevent EventHandler Raised;
}
מתקמפל מאחורי הקלעים למשהו כזה:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
publicclassEventRaiser
{
private EventHandler mRaised;
publicevent EventHandler Raised
{
add
{
this.mRaised += value;
}
remove
{
this.mRaised -= value;
}
}
}
בדומה לProperties שמתמקפלים לפונקציות מהצורה get_Property או set_Property, Eventים מתמקפלים לפונקציות מהצורה add_Event וremove_Event.
בשונה מProperties, בEventים אין לנו את החופש לבחור האם לממש רק את אחד הAccessorים (add או remove), אלא אנחנו מוכרחים לממש את שניהם, מאחר ומי שמשתמש במחלקה שלנו מבחוץ רואה שאנחנו חושפים Event ולכן מבחינתו אנחנו חושפים את שתי האפשרויות.
בד"כ לא נרצה לכתוב מימוש משלנו בתוך הAccessorים אלא במקרים מיוחדים, כמו שחשוב לנו שרישום יהיה Thread-safe, או לחלופין כאשר נרצה לוגיקה משלנו (למשל כשאנחנו חושפים Event של מישהו אחר)
רובנו מכירים את המושג ששמו Delegate מעולם הC# - זהו אובייקט המייצג מצביע לפונקציה כלשהי.
הטיפוס הזה מאפשר לנו לעשות דברים מאוד חזקים וראינו דוגמאות בעבר (למשל, טיפים על Anonymous delegates – מספרים 41-45).
אנחנו מכירים גם את המושג ששמו Event, אבל האם אי פעם תהיתם מה ההבדל בין Event לDelegate?
ובכן Event הוא סוג של Delegate, אלא שהקומפיילר מגביל את השימוש בו.
מה זה אומר? Delegate הוא טיפוס C#י לכל דבר, וזה אומר שאנחנו יכולים להחזיק Reference אליו, לשלוח אותו לפונקציה, להפעיל מתודה שלו ובעצם לעשות איתו כמעט כל דבר שאנחנו יכולים לעשות עם כל אובייקט C# אחר.
בפרט אם יש לנו Member שהוא מטיפוס Delegate כלשהו, אנחנו יכולים להפוך אותו להוסיף ולהסיר מתודות שהוא יקרא להן ע”י האופרטורים += ו-=.
לעומת זאת, לEvent הקומפיילר מתייחס בצורה אחרת – בתוך המחלקה הEvent הוא Delegate לכל דבר, אנחנו יכולים לעשות איתו כמעט כל מה שאנחנו יכולים לעשות עם כל אובייקט C#י אחר.
אלא שמבחינת העולם החיצון, כלומר טיפוסים שאינם המחלקה שלנו (אפילו אם הם יורשים ממנה), אנחנו יכולים לבצע בדיוק שתי פעולות: הרשמה לEvent והסרת רישום מEvent.
לדוגמה, נניח שיש לנו את המחלקה הזאת:
1
2
3
4
publicclassEventRaiser
{
publicevent EventHandler Raised;
}
אז בתוך המחלקה נוכל לעשות עם הEvent מה שבא לנו:
1
2
3
4
5
6
7
8
9
10
11
publicclassEventRaiser
{
publicevent EventHandler Raised;
privatevoidWhoKnew()
{
MethodInfo raisedMethod = this.Raised.Method;
this.Raised(this, EventArgs.Empty);
this.Raised = null;
}
}
מחוץ למחלקה לעומת זאת, אף שורה בפונקציה זו לא תתקמפל:
1
2
3
4
5
EventRaiser raiser = new EventRaiser();
MethodInfo raisedMethod = raiser.Raised.Method;
raiser.Raised(raiser, EventArgs.Empty);
raiser.Raised = null;
EventHandler raisedEvent = raiser.Raised;
נקבל שגיאת קימפול
The event ‘EventRaiser.Raised’ can only appear on the left hand side of += or -= (except when used from within the type ‘EventRaiser’)
כלומר, הקומפיילר מאפשר לנו רק להוסיף או להסיר רישומים לEvent.
הדבר דומה להבדל בין Property לField.
בתוך המחלקה אנחנו יכולים לעשות עם הField שלנו מה שבראש שלנו. ברגע שאנחנו מייחצנים Property, אנחנו יותר מוגבלים. למשל, אנחנו יכולים למנוע שינוי של הProperty, אלא רק לאפשר קריאה.
בכל אופן חשוב לשים לב לדברים הבאים:
בתוך המחלקה שלו, Event הוא Delegate לכל דבר. בין השאר, אפשר גם לאפס את הערך שלו, וכאשר הinstance שלנו מת אנחנו יכולים לאפס את הEvent בתוך המחלקה, כך שאין צורך לדאוג שכל מי שנרשם אלינו יסיר את הרישום וכו’.
בנוסף, Event זה בסה"כ הכמסה של הקומפיילר – כאשר אנחנו משתמשים בEvent, מאחורי הקלעים יש לנו Delegate, כך שמבחינה טכנית, אין דבר שאפשר לעשות עם Event ואי אפשר לעשות עם Delegate. טעות נפוצה היא לחשוב שDelegate מאפשר רישום רק למתודה אחת ואילו Event מאפשר רישום לכמה מתודות. זה לא נכון – גם Delegate מאפשר רישום לכמה מתודות. (בעזרת האופרטור +=, או לחלופין בעזרת שימוש בפונקציה Delegate.Combine היוצרת Delegate חדש שקורא לכל הDelegateים אחד אחרי השני)
ראינו בפעמים הקודמות כיצד ניתן להתגבר על הבעיה של פרסור וכתיבת תאריכים למחרוזות.
הבעיה העיקרית בפתרונות שראינו היא שבכל קריאה לToString ולDateTime.ParseExact (או לDateTime.Parse), אנחנו צריכים לציין את הCultureInfo או את הפורמט שאנחנו מעוניינים להשתמש בו.
הבעיה בזה היא שזה מכער לנו קצת את הקוד וגם שקל לשכוח לעשות את זה.
מסתבר שקיים הProperty ששמו CurrentCulture של Thread המאפשר לנו לציין עם איזה CultureInfo אנחנו מעוניינים לעבוד בThread הספציפי.
למשל,
1
2
CultureInfo usCulture = new CultureInfo("en-us", false);
Thread.CurrentThread.CurrentCulture = usCulture;
כעת אם נבצע את השורות הבאות נקבל:
1
2
3
DateTime rememberRemember = new DateTime(1997, 11, 5);
כך נצטרך לאתחל את הCultureInfo רק במקום אחד עבור כל Thread.
למרבה הצער, אין Property דומה המאפשר לנו לאתחל את הCultureInfo עבור כל הProcess, ולכן צריך לחשוב על פתרון לשאלה היכן מתאחלים אותו.
אופציה אחת היא ליצור Factory מיוחד של Threadים שידע לאתחל לThreadים את הProperty הזה בCultureInfo המתאים, וכל פעם שמישהו יבקש Thread הוא יצטרך לעבור דרך הFactory הזה.
אופציה נוספת היא בכל מחלקה שבה מתעסקים בפרסור תאריכים לאתחל מחדש את הCultureInfo, אבל זה יותר מכוער.
ראינו בפעם הקודמת כיצד אפשר להתגבר על הבעיה של הגדרות המחשב בעת פרסור מחרוזת לDateTime בעזרת ציון הפורמט איתו אנחנו מעוניינים לכתוב/לפרסר את התאריך.
אופציה נוספת היא להשתמש במשהו שנקרא CultureInfo. הטיפוס הזה מייצג מידע על Culture. הוא מכיל בעצם הגדרות שדומות להגדרות האזוריות של המחשב שלנו.
מה שאפשר לעשות זה לציין עם איזה סוג הגדרות אנחנו מעוניינים לכתוב ולפרסר את התאריכים. השימוש הוא די פשוט:
השגת הCultureInfo הרצוי:
1
CultureInfo israelCulture = new CultureInfo("he-il", false);
כתיבה:
1
2
3
4
DateTime rememberRemember = new DateTime(1997, 11, 5);
string rememberRememberString =
rememberRemember.ToString(israelCulture);
פרסור:
1
2
3
DateTime rememberRemember =
DateTime.Parse(rememberRememberString,
israelCulture);
שימו לב שאנחנו מעבירים false בConstructor של CultureInfo. זאת מאחר ואחרת ההגדרות של המשתמש דורסות את ההגדרות הדיפולטיות. מה זאת אומרת? נניח שהמשתמש החליט לבחור שהפורמט שהוא רוצה להציג בו את התאריך הוא פורמט לפי התאריך העברי,
במידה ולא נעביר false בConstructor, מה שיקרה זה שכשנקרא לToString, יודפס לנו התאריך העברי, ולא התאריך לו ציפינו. כאשר אנחנו מעבירים false זה בעצם אומר שאנחנו מעוניינים שהמשתמש לא ידרוס את ההגדרות הדיפולטיות, אלא להשתמש באמת בהגדרות הדיפולטיות.