object result = FirstOrDefault((dynamic) acceptableNames); // Jason
מה שקורה בעצם זה שבזמן ריצה מתבצע חיפוש של הOverload המתאים ביותר.
בנוסף החיפוש של המתודה המתאימה ביותר מתבצע תוך כדי התחשבות בפרמטר הגנרי, כמו שקריאה לפונקציה עם פרמטר גנרי Implicitly בזמן קימפול מוצאת את הOverload הטוב ביותר, ויודעת למצוא את הפרמטר הגנרי Implicitly. (ראו גם טיפ מספר 28)
אז גם כאן, נשלח הפרמטר הגנרי של הפונקציה באופן שקוף, לפי הפרמטר המתאים ביותר בזמן ריצה.
למה? שלחנו אמנם את firstShape בתור פרמטר דינאמי, אבל את secondShape שלחנו בתור Shape רגיל. הקומפיילר כותב קוד שמחפש בזמן ריצה את הפונקציה המתאימה ביותר עם הדרישות הבאות:
השם שלה הוא InscribeInShape. הפרמטר הראשון שלה צריך להיות מסוגל להתמודד עם הטיפוס של firstShape (כלומר להיות Triangle ומטה) והפרמטר השני שלה צריך להיות Shape ומטה.
מה שקורה כאן בעצם זה מאחר והעברנו את secondShape בתור טיפוס סטטי, הקומפיילר כותב קוד שמחפש פונקציה שיכולה לקבל את הטיפוס הסטטי הנ"ל (ולא "מגדיל ראש" ומשתמש בטיפוס הקונקרטי של secondShape)
איך אפשר לפתור? כמו שראינו בפעם הקודמת, אם נעביר את שני האובייקטים באופן דינאמי למתודה, תהיה התחשבות בטיפוסים שלהם בזמן ריצה.
טוב, מחלקת העזר היא מחלקה סטטית פנימית השומרת CallSiteים. המטרה שלה היא שבכל פעם שנבצע איזושהי פעולה דינאמית (מתוך המחלקה שאנחנו נמצאים בה עכשיו), היא תשמור Cache שלה, כך שאם נקרא לAdd הזאת למשל עוד כמה פעמים, בפעמים הנוספות (הפעמים שהן לא הפעם הראשונה), ההפעלה תהיה יותר מהירה.
למחלקת העזר יש Memberים של כל הפעולות שביצענו. הMemberים האלה הם מסוג CallSite.
מהו CallSite? תכלס זו איזושהי מחלקה שיוצרת Delegateשאנחנו קוראים לו בסופו של דבר בקריאה הדינאמית. הDelegate נשמר בMember בשם Target.
במקרה שלנו, למשל, מאחר ואנחנו לא מנסים להכניס את ערך ההחזר שלנו מהפונקציה, נוצר Delgate מסוג Action. (זהו Delegate שלא מחזיר ערך!)
הDelegate מקבל את הCallSite, את האובייקט שעליו אנחנו מנסים להפעיל את הפונקציה (זהו משתנה מסוג object) ואת הפרמטרים שאנחנו רוצים לשלוח לפונקציה.
את הCallSite מאתחלים בתוך הif. באמצעות הפונקציה CallSite<T>.Create. נתעלם שנייה מהפרמטר שמועבר שם ונראה איך זה נראה:
בסופו של דבר מה שקורה זה שנקראת הפונקציה בשם MakeUpdateDelegate היוצרת את הProperty בשםTarget אותו אנחנו מפעילים.
איך הפונקציה עובדת? היא בודקת אם הקריאה שביצענו היא פשוטה (כלומר בלי ref וoutים). במידה וכן, היא פשוט מחזירה קריאה לפונקציה מתאימה במחלקה פנימית בשם UpdateDelegates.
(לאחת מהפונקציות בשם UpdateAndExecute, UpdateAndExecuteVoid או NoMatch משורשר עם מספר הפרמטרים שיש בקריאה, בהתאם לסוג הפונקציה שקראנו לה והאם נמצאה פונקציה מתאימה)
(אם הפונקציה לא פשוטה, היא עושה משהו יותר מסובך (יוצרת פונקציה מתאימה בזמן ריצה))
מה הפונקציות האלה עושות? אלה פונקציות שככל הנראה מחוללות.
מה שהן עושות זה מעדכנות בCallSite שלנו את הTarget. (לא כל כך ברורה הנקודה הזאת)
אחרי זה הן מוצאות בעזרת הBinder של המחלקה את המתודה שצריך להריץ. מה זה הBinder הזה?
זה לא הBinder שהכרנו לא מזמן, אלא מדובר בCallSiteBinder, איזשהו אובייקט שממפה לנו איך להריץ פעולה דינאמית.
מה זאת אומרת? אם נסתכל, למשל העברנו אליו איזשהו Binder הנוצר מBinder.InvokeMember. זהו Binder שיודע להריץ מתודה של C#.
באופן דומה יש Binderים המסוגלים לעשות דברים אחרים למשל, להריץ פעולה בינארית, להפעיל אינדקסר, לבצע קריאה/כתיבה לMember של הטיפוס וכו’.
המחלקה הסטטית Binder היא ספציפיות לC#. כל שפה שרוצה תמיכה בDynamic Binding, צריכה לספק CallSiteBinder ספציפי לשפה.
למשל, CallSiteBinder של שפת C# ידאג לכך שכשאנחנו מבצעים את פעולת החילוק 5/2, יחזור לנו 2. לעומת זאת CallSiteBinder של שפה אחרת (למשל IronPython) יכול לדאוג לכך שיחזור לנו דווקא 2.5.
באופן כללי, לא כל כך פשוט להבין איך כל המנגנון הזה עובד מבפנים.
זה יעבוד, אבל לא באמת פתרנו את הבעיה: ראשית יש לנו כאן שכפול קוד. שנית, מה אם הפונקציה GetIntegerListOrStack יכולה להחזיר לנו עוד טיפוסים עם פונקצית Add?
עד Framework 4.0 יכולנו לפתור בעיה זו רק באמצעות Reflection.
RuntimeBinderException was unhandled: ‘IntegerList’ does not contain a definition for ‘Test’.
כלומר, נעוף בזמן ריצה, כיוון שלא קיים Member כזה.
הFeature הזה מאפשר לנו כתיבת קוד דינאמי שהוא יותר קריא, אבל לכן יש לנו יותר אחריות לשים לב שאנחנו באמת יודעים מה אנחנו עושים.
עם זאת, הFeature הזה לא מחליף לנו את Reflection באופן מלא.
אחת הסיבות היא שאנחנו יכולים לקרוא בעזרת dynamic לMemberים שהם public בלבד, ולא לכל Member של הטיפוס. (זהו גם יתרון, אבל מצד שני, לפעמים יש סיבות טובות לקרוא לMemberים שהם לא בהכרח public)
בנוסף, הדבר הזה לא מאפשר לנו לבצע ניתוח על האובייקט, למשל, לא נוכל לדעת באמצעות dynamic בלבד איזה Memberים יש לאיזשהו טיפוס, איזה Attributeים יש להם וכו’.
אם נסתכל עליה נראה שבסופו של דבר היא משתמשת במשהו שנקרא Binder כדי למצוא את המתודה המתאימה ביותר.
מה זה Binder?
אם נזכר בטיפים על Reflection (מספרים 136-151), אמרנו שמה שקורה בזמן קריאה לפונקציה בעזרת Reflection, הוא מיפוי של האובייקטים שאנחנו שולחים לפונקציה לפרמטרים שלה, באמצעות תהליך שנקרא Late Binding.
Binder הוא בעצם מנגנון שמאפשר לנו לשנות את המיפוי הזה.
הפונקציה הזאת מקבלת חתימה מערך של מתודות ומערך של טיפוסים, ומחפשת מבין כל המתודות את המתודה הכי מתאימה לחיפושים. זה עושה עבודה שמזכירה את העבודה שעושה הקומפיילר בזמן קימפול כדי למצוא את הOverload הכי מתאים לפרמטרים שהעברנו למתודה. אם אין כזה היא זורקת AmbiguousMatchException.
אם נסתכל בפונקציה GetMethodImpl, נראה שבעצם מה שהיא עושה זה קוראת לSelectMethod עם הטיפוסים שהיא קיבלה ועם מערך של כל המתודות שיש לטיפוס עם שם מסוים.
למה עוד זה שימושי? אני אכתוב שימוש שהיה לנו עם זה:
יש לנו מחלקה שיכולה לקבל כל מיני סוגים של הודעות כInput.
המשתמש יכול לטפל בהודעות מסוג מסוים במתודה משלו ע"י כתיבת מתודה בודדת והשמה של Attribute מסוים מעליה:
1
2
3
4
5
6
7
8
9
10
11
12
publicclassUserHandler
{
[Handler]
privatevoidHandleCircle(Circle circle)
{
}
[Handler]
privatevoidHandleDog(Dog dog)
{
}
}
ברגע שמגיעה הודעה היא יודעת להגיע למתודה (הבודדת) המתאימה.
בהתחלה זה מומש ע"י החזקה של Dictionary שממפה טיפוסים למתודות, ואז מפנה למתודה המתאימה ע"י חיפוש הGetType של האובייקט בDictionary.
אבל זה לא עבד. למה לא עבד? כי אם נניח הייתה לי חתימה כזאת:
1
2
3
4
[Handler]
privatevoidHandleShape(Shape shape)
{
}
אז זה לעולם לא היה נכנס אליה, כיוון שאין Instance שהטיפוס הקונקרטי שלו הוא Shape.
ככה למשל לTriangle וSquare לא היה שום טיפוס שיש לו מתודה מתאימה במחלקה שלי.
אפשר לחשוב על איך לממש משהו שישווה Typeים עפ"י עדיפות וימצא את הType הכי מתאים לType שהתקבל, אלא שזה קצת מסובך לממש דבר כזה…
במקום זאת, אפשר להשתמש בBinder שבא עם הFramework שימצא את המתודה עם החתימה הכי מתאימה! 😃
אלו מכם שינסו ליצור instance של Binder, יתאכזבו לשמוע שזו מחלקה אבסטרקטית, ושכל המימושים שלה בFramework הם internal (טיפ מספר 213).
מסתבר שאפשר להשתמש במימוש הדיפולטי ע"י גישה לProperty הסטטי Type.DefaultBinder.
זהו כאמור קונספט המאפשר לנו להחליט בזמן ריצה איזו מתודה להפעיל.
מה בדיוק Single כאן?
ובכן, ההחלטה איזו מתודה להפעיל בזמן ריצה, נקבע עפ”י פרמטר יחיד – הטיפוס של הinstance שעליו אנו מפעילים את המתודה.
קיים קונספט מוכלל דומה שנקרא Multiple dynamic dispatch. הרעיון הוא שהניתוב למתודה בזמן ריצה לא נקבע עפ”י פרמטר בודד (הטיפוס של הinstance עליו מופעלת המתודה), אלא עפ”י מספר פרמטרים, שהם חלק מהטיפוסים שנשלחו לפונקציה.
מה קורה כאן? אנחנו מחפשים בתוך המחלקה שלנו מתודה בשם InscribeInShape עם פרמטרים מהטיפוס של הפרמטרים שקיבלנו לפונקציה. (ראו גם טיפ מספר 137, 147)
לאחר מכן אנחנו מפעילים את הפונקציה עם הפרמטרים הנ"ל (ראו גם טיפ מספר 148)
מה הסוד כאן? הסוד הוא שהפונקציה GetMethod יודעת למצוא את הOverload שהכי מתאים לטיפוסים שאנחנו נותנים לה, בדומה לקומפיילר שבזמן קימפול מוצא את הOverload עם טיפוסי הפרמטרים הכי מתאימים.
מה זאת אומרת? אם, למשל, תהיה לנו את ההיררכיה הבאה:
מה שיפה פה זה שלמרות המימוש של הפונקציות האלה טרם נכתב, אנחנו כן יכולים לכתוב שלד לפונקציה, המבצע את המבוקש.
כעת מי שירש מהמחלקה שלנו, יקבל אוטומטית מימוש לפונקציה BuyDrink.
הדבר הזה מאוד חזק.
הוא מאפשר מספר דברים טובים:
מניעת שכפול קוד – במקום שכל אחד יממש בעצמו את הפונקציה הזאת, היא ממומשת פעם אחת בBase.
שינוי התנהגות של המחלקה ע"י ירושה ודריסה.
נקודות כניסה מוגבלות – במקום שהTemplate Method יהיה וירטואלי וכל אחד ידרוס אותו, אנחנו יכולים להגביל אותו, ולאפשר לדרוס רק את הפונקציות שהוא משתמש בהן.
הכרנו פעם שעברה את המושג של מתודה וירטואלית, המאפשרת לאפליקציה שלנו להחליט בזמן ריצה לאיזו פונקציה לקרוא.
טעות נפוצה שיכולה להיווצר משימוש בפונקציות וירטואליות, היא קריאה להן בConstructor.
למה הכוונה?
הנה דוגמה קלאסית (ושכיחה!): נניח שיש לנו איזושהי פונקציה שמאתחלת לנו את אחד הMemberים של המחלקה. למשל, פונקציה שמאתחלת את הLogger של המחלקה שלנו:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
publicclassAnimal
{
private ILogger mLog;
publicAnimal()
{
this.mLog = GetLogger();
}
private ILogger GetLogger()
{
returnnew FileLogger();
}
}
כעת, אנחנו מעוניינים שמי שיורש מהמחלקה שלנו יוכל להחליף את האתחול של הLogger בפונקציה משלו.
הדבר הראשון שעולה לנו לראש היא להפוך את הפונקציהGetLogger לprotected וvirtual:
1
2
3
4
protectedvirtual ILogger GetLogger()
{
returnnew FileLogger();
}
יופי, עכשיו כל אחד יכול לדרוס את הפונקציה הזאת ולעשות איתה מה שהוא רוצה, למשל:
1
2
3
4
5
6
7
publicclassDog : Animal
{
protectedoverride ILogger GetLogger()
{
returnnew BoneLogger();
}
}
נראה שהפתרון טוב, אלא שיש פה בעיה.
מה הבעיה? נניח שלמחלקה היורשת יש איזשהו פרמטר משלה, והלוגר מאותחל לפיו:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
publicclassDog : Animal
{
privatestring mBark;
publicDog(string bark)
{
mBark = bark;
}
protectedoverride ILogger GetLogger()
{
returnnew BarkLogger(mBark);
}
}
נראה תקין, נכון?
למרות זאת, מי שינסה להריץ את הקוד הזה, יגלה שאנחנו שולחים לBarkLogger את הערך null תמיד.
טוב, אז מה קורה פה?
כזכור, כאשר מתבצעת קריאה לConstructor של המחלקה שלנו, קודם מתבצעת הקריאה לאבות, והקריאה למחלקה שלנו מתבצעת אחרונה:
למשל, אם יש לנו את הירושה הבאה:
1
Labrador : Dog : Animal : object
אז קודם כל יקרא הConstructor של object, אחר כך הConstructor של Animal, אז הConstructor של Dog, ולבסוף הConstructor של Labrador.
עכשיו, עצם העובדה שקראנו לפונקציה וירטואלית בConstructor של Animal, גורם לכך שנכנסנו אליה, לפני שבכלל נכנסנו לConstructor של Dog, ולכן הMemberים של Dog אינם עדיין מאותחלים, ולכן תמיד נקבל בmBark את הערך הדיפולטי.
מה אפשר לעשות?
האופציה הראשונה היא להשאיר את זה ככה – זו אופציה שמומלצת רק אם שאר האופציות לא תופסות, ורק אם המחלקה הנ"ל לא אמורה להידרס ע"י מפתחים אחרים (כלומר היא סוג של מחלקה פנימית). ברגע שהמחלקה היא תשתיתית ומשתמשת הרבה מפתחים, האופציה הזאת לא טובה – כיוון שיש סיכוי גבוה שהם יתקלו כל פעם מחדש בבעיה כזו.
האופציה השנייה היא לפעפע את הפרמטרים כלפי מעלה. למשל, במקרה שלנו, לדאוג לכך שהConstructor של Animal יקבל ILogger בConstructor. זה יראה בערך כך:
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
28
publicclassAnimal
{
private ILogger mLog;
publicAnimal(ILogger logger)
{
this.mLog = logger;
}
publicAnimal() : this(new FileLogger())
{
}
}
publicclassDog : Animal
{
privatestring mBark;
publicDog(string bark): base(GetLogger(bark))
{
mBark = bark;
}
privatestaticILoggerGetLogger(string bark)
{
returnnew BarkLogger(bark);
}
}
הפתרון מונע קריאה לפונקציות וירטואליות בConstructor, אבל הוא יותר מסובך, אנחנו קוראים לפונקציה (או Constructor אחר) בקריאה לbase. (ראו גם טיפ מספר 88)
בד"כ זה לא כל כך פשוט להעביר את הפרמטרים למעלה.
האופציה השלישית היא ליצור פונקצית Init, שתדאג לאתחל את האובייקט:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
publicclassAnimal
{
private ILogger mLog;
publicAnimal()
{
}
protectedvirtualvoidInit()
{
this.mLog = GetLogger();
}
protectedvirtual ILogger GetLogger()
{
returnnew FileLogger();
}
}
ואז לאתחל את האובייקטים שלנו עם איזשהו Factory:
1
2
3
4
publicinterfaceIAnimalFactory
{
TAnimal GetAnimal<TAnimal>() where TAnimal : Animal, new();
}
עם מימוש כזה:
1
2
3
4
5
6
7
8
9
privateclassAnimalFactory : IAnimalFactory
{
public TAnimal GetAnimal<TAnimal>() where TAnimal: Animal,new()
{
TAnimal animal = new TAnimal();
animal.Init();
return animal;
}
}
כאן AnimalFactory היא מחלקה פנימית של Animal, אז היא יכולה לגשת לפונקציה הprotected ששמה Init.
ואז יש איזשהו Property שחושף את הFactory הזה, שאנחנו יכולים להשתמש בו:
1
Dog dog = Animal.Factory.GetAnimal<Dog>();
אמנם אין יותר קריאות וירטואליות בConstructor, ואין סכנה שניגש לMemberים של המחלקה לפני שאותחלו, אבל יש פה את החסרונות הבאים: כל היצירות של אובייקטים חייבות לעבור דרך הFactory הנ"ל. על כן, גם לכל המחלקות היורשות חייבת להיות אותה חתימה של Constructor (במקרה שלנו – Constructor דיפולטי). כך שהפתרון מסרבל את השימוש במחלקה שלנו.
פתרון רביעי הוא לוותר באופן כללי על הקריאה של פונקציה לפונקציה הוירטואלית בConstructor, ולדאוג שכל אחד יאתחל את הLogger בעצמו. היתרון בפתרון הזה הוא שהוא פשוט. החסרון בו הוא שהLogger מאותחל מספר פעמים (בכל Constructor הוא מאותחל ונדרס).
מאוד חשוב לשים לב לקריאה לפונקציות וירטואליות בConstructor. הReSharper מזהיר על כך.
בנוסף לדוגמאות שכתבתי, אתם עשויים להיתקל בבעיות כאלה כאשר תיצרו Mockים לאובייקטים שלכם לUnit Testים.
בC# כמו בשפות תכנות רבות וטובות, קיים משהו שנקרא single dynamic dispatch.
השם היותר מוכר הוא מתודה וירטואלית – הFeature הזה מאפשר לנו להחליט לאיזו פונקציה לקרוא בזמן ריצה.
מה זה אומר?
כאשר הקוד שלנו מתקמפל, הקומפיילר צריך לדעת לאן לנתב את הקריאות לפונקציות שלנו.
יש פונקציות שכבר בזמן קימפול, הקומפיילר יודע את הכתובת שאליה הוא צריך לגשת כדי לקרוא להן, למשל הפונקציה הבאה:
1
2
3
4
5
6
7
publicclassMyClass
{
publicintAdd(int x, int y)
{
return (x + y);
}
}
מאחר והפונקציה לא וירטואלית, הקומפיילר יודע את הכתובת שלה כבר זמן קימפול ולכן יכול לבצע קריאה אליה ישירות.
לעומת זאת, בקריאה למתודה וירטואלית, הקומפיילר לא יודע לאן הוא אמור להפנות אותנו:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
publicclassShape
{
publicvirtualvoidDraw()
{
// ...
}
}
publicclassEllipse : Shape
{
publicoverridevoidDraw()
{
Console.WriteLine("A very cool ellipse");
}
}
publicclassCircle : Ellipse
{
publicoverridevoidDraw()
{
Console.WriteLine("A blue circle");
}
}
כעת כשיש לנו קוד כזה:
1
2
Shape shape = GetRandomShape();
shape.Draw();
הקומפיילר לא יודע לאיזו פונקציה הוא אמור להפנות אותנו.
למה? הקריאה לDraw מתבצעת עפ"י הטיפוס של shape בזמן ריצה.
אם הטיפוס הקונקרטי שלנו הוא מסוג Ellipse, ייכתב למסך "A very cool ellipse", ואילו אם הוא Circle, ייכתב למסך "A blue circle".
כעת shape יכול להיות כל דבר, והקומפיילר לא יודע לאיזו צורה הReference שלנו מצביע.
אז מה הוא עושה? הוא מחליט בזמן ריצה לאן להפנות אותנו.
איך זה עובד? מוחזק בצד מבנה נתונים בשם "Virtual table" המנתב טיפוס למתודה שהוא אמור להפעיל. כעת בקריאה לפונקציה בזמן ריצה, ניגש למבנה זה ובודקים איזו מתודה יש להפעיל. (ראו טיפים 319-321)
אז למה זה טוב?
מי שלמד OOP בוודאי יודע שפונקציות וירטואליות משחקות תפקיד חשוב בפולימורפיזם. ("פולימורפיזם זה הswitch-case של OOP")
הן בעצם מאפשרות למתכנתים שיורשים מהמחלקה שלנו להשפיע על ההתנהגות שלה.
הדבר מאוד חזק, אבל גם יכול להיות מסוכן אם לא משתמשים בו נכון.
עצם העובדה שאנחנו הופכים פונקציה לוירטואלית, מאפשרת לכל אחד לכתוב מה שהוא רוצה שם, כך שקריאה לפונקציה וירטואלית מסכנת אותנו, בכך שאנחנו לא יודעים מה הולך לקרות בה.
לכן חשוב מאוד לבחון איזה פונקציות הופכים לוירטואליות ואילו לא. (בC# פונקציות הן לא וירטואליות דיפולטית, לעומת Java בה הן כן)
בנוסף, כדאי לשים לב שקריאה לפונקציה וירטואלית היא יותר איטית מקריאה לפונקציה רגילה, מאחר ומתבצעת גישה לVirtual table.