230. Generic methods and dynamic binding

אז כמו שראינו בפעמים הקודמות,

קריאה דינאמית לפונקציה מאפשרת לנו לקרוא בזמן ריצה לOverload המתאים ביותר לפרמטרים שאנחנו שולחים.

מה שאולי לא הכי טריוויאלי מהסיפור הזה, הוא שאפשר גם לקרוא בצורה דינאמית לפונקציה גנרית ככה:

למשל, נניח שיש לנו פונקציה כזאת:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static T FirstOrDefault<T>(IEnumerable<T> enumerable)
{
IEnumerator<T> enumerator = enumerable.GetEnumerator();
if (!enumerator.MoveNext())
{
return default(T);
}
else
{
return enumerator.Current;
}
}

אז אם נכתוב קוד כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
object acceptableNames =
new string[]
{
"Jason",
"Jefferson",
"Jackson",
"Washington",
"Washington Santos",
"Ferguson",
"Robert Louis Stevenson",
"Michael",
"Jacobs"
};
object result = FirstOrDefault((dynamic) acceptableNames); // Jason

מה שקורה בעצם זה שבזמן ריצה מתבצע חיפוש של הOverload המתאים ביותר.

בנוסף החיפוש של המתודה המתאימה ביותר מתבצע תוך כדי התחשבות בפרמטר הגנרי, כמו שקריאה לפונקציה עם פרמטר גנרי Implicitly בזמן קימפול מוצאת את הOverload הטוב ביותר, ויודעת למצוא את הפרמטר הגנרי Implicitly. (ראו גם טיפ מספר 28)

אז גם כאן, נשלח הפרמטר הגנרי של הפונקציה באופן שקוף, לפי הפרמטר המתאים ביותר בזמן ריצה.

שיהיה סופ"ש דינאמי גנרי טוב!

שתף

229. Static types and dynamic binding

בהמשך לפעם הקודמת:

ראינו שאם אנחנו מעבירים פרמטרים דינאמיים לפונקציה, מתבצע חיפוש של הOverload המתאים ביותר לפונקציה שלנו.

מה שמפתיע הוא שיש גם התחשבות בטיפוסים הסטטיים המשתתפים בקריאה לפונקציה:

נמשיך עם הדוגמה של פעם שעברה:

יש לנו את הפונקציות הבאות:

1
2
public static void InscribeInShape(Triangle innerShape, Circle circumscribedShape)
public static void InscribeInShape(Circle innerShape, Triangle circumscribedShape)

להן נוסיף את הOverloadים הבאים:

1
2
public static void InscribeInShape(Shape innerShape, Circle circumscribedShape)
public static void InscribeInShape(Triangle innerShape, Shape circumscribedShape)

מה יקרה כאשר נריץ את השורה הבאות?

1
2
3
Shape firstShape = new Triangle();
Shape secondShape = new Circle();
InscribeInShape((dynamic)firstShape, secondShape);

מה שאולי טבעי לחשוב זה שנכנס לOverload של

1
public static void InscribeInShape(Triangle innerShape, Circle circumscribedShape)

שהרי שלחנו את הTriangle בתור פרמטר דינאמי.

ובכן, דווקא נכנס לOverload של

1
public static void InscribeInShape(Triangle innerShape, Shape circumscribedShape)

למה? שלחנו אמנם את firstShape בתור פרמטר דינאמי, אבל את secondShape שלחנו בתור Shape רגיל. הקומפיילר כותב קוד שמחפש בזמן ריצה את הפונקציה המתאימה ביותר עם הדרישות הבאות:

השם שלה הוא InscribeInShape. הפרמטר הראשון שלה צריך להיות מסוגל להתמודד עם הטיפוס של firstShape (כלומר להיות Triangle ומטה) והפרמטר השני שלה צריך להיות Shape ומטה.

מה שקורה כאן בעצם זה מאחר והעברנו את secondShape בתור טיפוס סטטי, הקומפיילר כותב קוד שמחפש פונקציה שיכולה לקבל את הטיפוס הסטטי הנ"ל (ולא "מגדיל ראש" ומשתמש בטיפוס הקונקרטי של secondShape)

איך אפשר לפתור? כמו שראינו בפעם הקודמת, אם נעביר את שני האובייקטים באופן דינאמי למתודה, תהיה התחשבות בטיפוסים שלהם בזמן ריצה.

שיהיה המשך יום דינאמי טוב

שתף

228. Dynamic member overload resolution

ראינו קודם שאפשר לקרוא בצורה דינאמית למתודה שלInstance, והיא תפוענח בזמן ריצה.

לפני כמה זמן סיפרתי קצת על Multiple dynamic dispatchואמרתי שבC# 4.0 אפשר לקבל את ההתנהגות הנ”ל בצורה קלה יותר.

איך עושים את זה?

תזכורת: יש לנו את הOverloadים הבאים:

1
2
3
4
public static void InscribeInShape(Square innerShape, Ellipse circumscribedShape)
public static void InscribeInShape(Ellipse innerShape, Square circumscribedShape)
public static void InscribeInShape(Triangle innerShape, Circle circumscribedShape)
public static void InscribeInShape(Circle innerShape, Triangle circumscribedShape)

ואנחנו רוצים שהפונקציה הבאה:

1
public static void InscribeInShape(Shape innerShape,Shape circumscribedShape)

תדע לנתב בזמן ריצה את הצורות לOverload המתאים ביותר. (ראו גם טיפ מספר 224)

איך עושים זאת?

הFeature של Dynamic binding גם מאפשר לנו למצוא את הOverload הטוב ביותר של מתודה.

1
2
3
4
public static void InscribeInShape(Shape innerShape, Shape circumscribedShape)
{
InscribeInShape((dynamic)innerShape, (dynamic)circumscribedShape);
}

מה שקורה כאן, זה שבגלל שעשינו "הסבה" של שני הפרמטרים לdynamic, הקומפיילר כותב קוד שגורם למציאת הOverloadהמתאים ביותר והפעלתו.

משהו כזה (לא ממש יפה)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void InscribeInShape(Shape innerShape, Shape circumscribedShape)
{
if (InscribeInShapeCallSite == null)
{
InscribeInShapeCallSite =
CallSite<Action<CallSite, Type, object, object>>.Create(
Binder.InvokeMember(CSharpBinderFlags.ResultDiscarded, "InscribeInShape", null, typeof (ShapeUtilities),
new CSharpArgumentInfo[]
{
CSharpArgumentInfo.Create(
CSharpArgumentInfoFlags.IsStaticType |
CSharpArgumentInfoFlags.UseCompileTimeType, null),
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
}));
}
InscribeInShapeCallSite.Target.Invoke(InscribeInShapeCallSite, typeof (ShapeUtilities), innerShape,
circumscribedShape);
}

נעזוב לרגע את מה שקורה מאחורי הקלעים – יש בעיה מסוימת בפתרון שכתבתי פה:

אם נעביר לפונקציה פרמטרים בלי overload מתאים, אז הOverload הכי מתאים של הפונקציה תהיה הפונקציה עצמה.

זה גורם לרקורסיה אינסופית, מה שאנחנו ממש לא רוצים.

מה אפשר לעשות במקום? פתרון לגיטימי הוא לעשות שהמימוש של

1
public static void InscribeInShape(Shape innerShape, Shape circumscribedShape)

יהיה פשוט:

1
2
3
4
public static void InscribeInShape(Shape innerShape, Shape circumscribedShape)
{
throw new ArgumentException("No suitable overload was matched");
}

וליצור פונקציה אחרת שתעשה את מה שעשינו קודם:

1
2
3
4
public static void InscribeShapeInOther(Shape innerShape, Shape circumscribedShape)
{
InscribeInShape((dynamic)innerShape, (dynamic)circumscribedShape);
}

זה קצת שובר את היופי, אבל מונע רקורסיה אינסופית.

המשך יום דינאמי טוב

שתף

227. How dynamic binding works

פעם קודמת הכרנו את הFeature החדש של C# 4.0 ששמו dynamic binding.

הפעם ננסה להבין איך הקסם שם עובד.

אז ניקח את הקוד שכתבנו פעם שעברה:

1
2
3
object integerDataStructure = GetIntegerListOrStack();
dynamic dynamicDataStructure = integerDataStructure;
dynamicDataStructure.Add(6);

ונסתכל מה קורה כשאנחנו פותחים אותו בReflector: נקבל משהו כזה:

נשים לב קודם כל שנוצרת לנו מחלקה סטטית פנימית חדשה:

1
2
3
4
private static class MyClassHelper
{
public static CallSite<Action<CallSite, object, int>> AddActionOnObjectAndInt;
}

והקוד שכתבנו מתרגם למשהו כזה: (שיניתי את השמות כדי שיהיה אפשר להבין)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
object integerDataStructure = GetIntegerListOrStack();
object dynamicDataStructure = integerDataStructure;
if (MyClassHelper.AddActionOnObjectAndInt == null)
{
MyClassHelper.AddActionOnObjectAndInt =
CallSite<Action<CallSite, object, int>>.Create(Binder.InvokeMember(
CSharpBinderFlags.ResultDiscarded, "Add", null, typeof (MyClass),
new CSharpArgumentInfo[]
{
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
CSharpArgumentInfo.Create(
CSharpArgumentInfoFlags.Constant | CSharpArgumentInfoFlags.UseCompileTimeType, null)
}));
}
MyClassHelper.AddActionOnObjectAndInt.Target.Invoke(MyClassHelper.AddActionOnObjectAndInt, dynamicDataStructure, 6);

טוב, מחלקת העזר היא מחלקה סטטית פנימית השומרת 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.


באופן כללי, לא כל כך פשוט להבין איך כל המנגנון הזה עובד מבפנים.

אישית, לא הצלחתי לעקוב עד הסוף.

המשך יום דינאמי טוב

שתף

226. Dynamic binding

לעתים קורה שאנחנו יודעים בזמן קימפול, שפונקציה מסוימת קיימת, אבל הקומפיילר לא יודע זאת.

לדוגמה, נניח שיש לנו שתי מחלקות כאלה:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class IntegerList
{
public void Add(int value)
{
}
}
public class IntegerStack
{
public void Add(int value)
{
}
}

ונניח שיש פונקציה שמחזירה אחת מהן:

1
2
3
4
public static object GetIntegerListOrStack()
{
// ...
}

כעת נניח שקראנו לפונקציה הזו:

1
object integerDataStructure = GetIntegerListOrStack();

ואנחנו רוצים להפעיל את הפונקציה Add. לא נוכל, מאחר ואנו לא יודעים איזה סוג אובייקט חזר, והמחלקות לא מממשות ממשק משותף.

נוכל לעשות משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
object integerDataStructure = GetIntegerListOrStack();
if (integerDataStructure is IntegerList)
{
IntegerList list = (IntegerList)integerDataStructure;
list.Add(6);
}
else if (integerDataStructure is IntegerStack)
{
IntegerStack stack = (IntegerStack)integerDataStructure;
stack.Add(6);
}

זה יעבוד, אבל לא באמת פתרנו את הבעיה: ראשית יש לנו כאן שכפול קוד. שנית, מה אם הפונקציה GetIntegerListOrStack יכולה להחזיר לנו עוד טיפוסים עם פונקצית Add?


עד Framework 4.0 יכולנו לפתור בעיה זו רק באמצעות Reflection.

אלא שבReflection כתיבה כזאת היא לא הכי כיפית:

1
2
3
4
5
6
7
8
object integerDataStructure = GetIntegerListOrStack();
Type dataStructureType = integerDataStructure.GetType();
MethodInfo addMethod =
dataStructureType.GetMethod("Add", new[] { typeof(int) });
addMethod.Invoke(integerDataStructure, new object[] { (int)6 });

קריאה לפונקציה פשוטה, לקחה פה 3 שורות קוד.

בC# 4.0 נוסף Feature לשפה בשם Dynamic binding. הוא מאפשר לנו לבצע פעולה דומה לזו בסינטקס יותר כיפי:

1
2
3
4
object integerDataStructure = GetIntegerListOrStack();
dynamic dynamicDataStructure = integerDataStructure;
dynamicDataStructure.Add(6);

הקוד הזה בעצם יבצע בסופו של דבר אותה פעולה – הפעלת הפונקציה Add עם הפרמטר 6.

מה קורה פה?

הdynamic הזה הוא Keyword שאומר לקומפיילר "אני יודע מה אני עושה".

עצם העובדה שיש לנו משתנה "מטיפוס dynamic", מאפשרת לנו להפעיל איזו פונקציה שאנחנו רוצים עליו.

מה שקורה בפועל זה שבזמן ריצה מתבצע חיפוש של הMemberשאני רוצה להפעיל, ובמידה והוא קיים, הוא יבצע את הקריאה אליו.


מה קורה אם הMember לא קיים? בואו ננסה:

1
2
dynamic dynamicDataStructure = integerDataStructure;
dynamicDataStructure.Test(6);

נקבל את הException הבא:

RuntimeBinderException was unhandled: ‘IntegerList’ does not contain a definition for ‘Test’.

כלומר, נעוף בזמן ריצה, כיוון שלא קיים Member כזה.


הFeature הזה מאפשר לנו כתיבת קוד דינאמי שהוא יותר קריא, אבל לכן יש לנו יותר אחריות לשים לב שאנחנו באמת יודעים מה אנחנו עושים.

עם זאת, הFeature הזה לא מחליף לנו את Reflection באופן מלא.

אחת הסיבות היא שאנחנו יכולים לקרוא בעזרת dynamic לMemberים שהם public בלבד, ולא לכל Member של הטיפוס. (זהו גם יתרון, אבל מצד שני, לפעמים יש סיבות טובות לקרוא לMemberים שהם לא בהכרח public)

בנוסף, הדבר הזה לא מאפשר לנו לבצע ניתוח על האובייקט, למשל, לא נוכל לדעת באמצעות dynamic בלבד איזה Memberים יש לאיזשהו טיפוס, איזה Attributeים יש להם וכו’.

שבוע דינאמי טוב!

שתף

225. Binder

פעם שעברה ראינו שהפונקציה GetMethod יודעת למצוא את הOverload שהכי מתאים לסוגי הפרמטרים שאנחנו מעבירים לה.

אני רוצה להסביר קצת על איך הפונקציה הזאת עובדת, כי זה יכול לעזור לנו לכתוב דברים דומים:

אם נסתכל בReflector נופנה להרבה פונקציות. בסופו של הדבר הפונקציה המעניינת היא הפונקציה

1
protected override MethodInfo GetMethodImpl(string name, BindingFlags bindingAttr, Binder binder, CallingConventions callConv, Type[] types, ParameterModifier[] modifiers)

של RuntimeType.

אם נסתכל עליה נראה שבסופו של דבר היא משתמשת במשהו שנקרא Binder כדי למצוא את המתודה המתאימה ביותר.

מה זה Binder?

אם נזכר בטיפים על Reflection (מספרים 136-151), אמרנו שמה שקורה בזמן קריאה לפונקציה בעזרת Reflection, הוא מיפוי של האובייקטים שאנחנו שולחים לפונקציה לפרמטרים שלה, באמצעות תהליך שנקרא Late Binding.

Binder הוא בעצם מנגנון שמאפשר לנו לשנות את המיפוי הזה.

ממה הוא מורכב?

הוא מכיל את הפונקציות הבאות:

1
2
3
4
5
6
7
8
9
public abstract class Binder
{
public abstract FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, object value, CultureInfo culture);
public abstract MethodBase BindToMethod(BindingFlags bindingAttr, MethodBase[] match, ref object[] args, ParameterModifier[] modifiers, CultureInfo culture, string[] names, out object state);
public abstract object ChangeType(object value, Type type, CultureInfo culture);
public abstract void ReorderArgumentArray(ref object[] args, object state);
public abstract MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers);
public abstract PropertyInfo SelectProperty(BindingFlags bindingAttr, PropertyInfo[] match, Type returnType, Type[] indexes, ParameterModifier[] modifiers);
}

מה אנחנו רואים כאן? לFieldInfo וMethodBaseשהכרנו מReflection יש בעצם איזושהי פונקציה מתאימה שעושה לנו את הBinding אליו.

בנוסף, יש פה פונקציה מעניינת שנקראת ChangeType הנקראת בעת קריאה למתודה (בעזרת Invoke), ומאפשרת לנו להשפיע על איך אובייקט ישלח לפונקציה שלנו.


מה שאותנו מעניין הוא דווקא הפונקציה

1
public abstract MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers);

הפונקציה הזאת מקבלת חתימה מערך של מתודות ומערך של טיפוסים, ומחפשת מבין כל המתודות את המתודה הכי מתאימה לחיפושים. זה עושה עבודה שמזכירה את העבודה שעושה הקומפיילר בזמן קימפול כדי למצוא את הOverload הכי מתאים לפרמטרים שהעברנו למתודה. אם אין כזה היא זורקת AmbiguousMatchException.

אם נסתכל בפונקציה GetMethodImpl, נראה שבעצם מה שהיא עושה זה קוראת לSelectMethod עם הטיפוסים שהיא קיבלה ועם מערך של כל המתודות שיש לטיפוס עם שם מסוים.


למה עוד זה שימושי? אני אכתוב שימוש שהיה לנו עם זה:

יש לנו מחלקה שיכולה לקבל כל מיני סוגים של הודעות כInput.

המשתמש יכול לטפל בהודעות מסוג מסוים במתודה משלו ע"י כתיבת מתודה בודדת והשמה של Attribute מסוים מעליה:

1
2
3
4
5
6
7
8
9
10
11
12
public class UserHandler
{
[Handler]
private void HandleCircle(Circle circle)
{
}
[Handler]
private void HandleDog(Dog dog)
{
}
}

ברגע שמגיעה הודעה היא יודעת להגיע למתודה (הבודדת) המתאימה.

בהתחלה זה מומש ע"י החזקה של Dictionary שממפה טיפוסים למתודות, ואז מפנה למתודה המתאימה ע"י חיפוש הGetType של האובייקט בDictionary.

אבל זה לא עבד. למה לא עבד? כי אם נניח הייתה לי חתימה כזאת:

1
2
3
4
[Handler]
private void HandleShape(Shape shape)
{
}

אז זה לעולם לא היה נכנס אליה, כיוון שאין Instance שהטיפוס הקונקרטי שלו הוא Shape.

ככה למשל לTriangle וSquare לא היה שום טיפוס שיש לו מתודה מתאימה במחלקה שלי.

אפשר לחשוב על איך לממש משהו שישווה Typeים עפ"י עדיפות וימצא את הType הכי מתאים לType שהתקבל, אלא שזה קצת מסובך לממש דבר כזה…

במקום זאת, אפשר להשתמש בBinder שבא עם הFramework שימצא את המתודה עם החתימה הכי מתאימה! 😃


אלו מכם שינסו ליצור instance של Binder, יתאכזבו לשמוע שזו מחלקה אבסטרקטית, ושכל המימושים שלה בFramework הם internal (טיפ מספר 213).

מסתבר שאפשר להשתמש במימוש הדיפולטי ע"י גישה לProperty הסטטי Type.DefaultBinder.

סופ"ש מעולה שלא כבול לשום דבר.

שתף

224. Multiple dynamic dispatch

דיברנו קצת על Single dynamic dispatch.

זהו כאמור קונספט המאפשר לנו להחליט בזמן ריצה איזו מתודה להפעיל.

מה בדיוק Single כאן?

ובכן, ההחלטה איזו מתודה להפעיל בזמן ריצה, נקבע עפ”י פרמטר יחיד – הטיפוס של הinstance שעליו אנו מפעילים את המתודה.

קיים קונספט מוכלל דומה שנקרא Multiple dynamic dispatch. הרעיון הוא שהניתוב למתודה בזמן ריצה לא נקבע עפ”י פרמטר בודד (הטיפוס של הinstance עליו מופעלת המתודה), אלא עפ”י מספר פרמטרים, שהם חלק מהטיפוסים שנשלחו לפונקציה.

למה הכוונה?

נניח שיש לנו את הפונקציה הבאה:

1
public static void InscribeInShape(Shape innerShape, Shape circumscribedShape)

זו פונקציה שמקבלת שתי צורות, וחוסמת צורה אחת בתוך השנייה.

עכשיו, אנחנו יכולים ליצור את מגוון הOverloadים הבאים:

1
2
3
4
public static void InscribeInShape(Square innerShape, Circle circumscribedShape)
public static void InscribeInShape(Triangle innerShape, Circle circumscribedShape)
public static void InscribeInShape(Circle innerShape, Triangle circumscribedShape)
public static void InscribeInShape(Circle innerShape, Square circumscribedShape)

אלה מספר אפשרויות של לחסום מעגל בתוך ריבוע או משולש, או לחסום משולש/ריבוע במעגל.

בזמן קימפול אם נכתוב קוד כזה:

1
2
3
Square square;
Circle circle;
InscribeInShape(square, circle);

הקומפיילר ידע לנתב אותנו לOverload המתאים ביותר, במקרה שלנו ל

1
public static void InscribeInShape(Square innerShape, Circle circumscribedShape)

היינו רוצים שגם אם נכתוב קוד כזה:

1
2
3
Shape someShape = GetRandomShape();
Shape someOtherShape = GetRandomShape();
InscribeInShape(someShape, someOtherShape);

ושהמתודה עם הOverload המתאים ביותר תבחר בזמן ריצה, שהרי כרגע תמיד נבחר הOverload

1
public static void InscribeInShape(Shape innerShape, Shape circumscribedShape)

לקונספט הזה של בחירת מתודה בזמן ריצה עפ"י סוגי הפרמטרים, קוראים Multiple dynamic dispatch.


איך אפשר להשיג את זה בC#? עד C# 3.0 צריך לעבוד קצת קשה כדי לעשות משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void InscribeInShape(Shape innerShape, Shape circumscribedShape)
{
if ((innerShape == null) || (circumscribedShape == null))
{
throw new ArgumentException();
}
MethodInfo bestOverload =
typeof (ShapeUtilities).GetMethod("InscribeInShape",
new[] {innerShape.GetType(), circumscribedShape.GetType()});
bestOverload.Invoke(null,
new[] {innerShape, circumscribedShape});
}

מה קורה כאן? אנחנו מחפשים בתוך המחלקה שלנו מתודה בשם InscribeInShape עם פרמטרים מהטיפוס של הפרמטרים שקיבלנו לפונקציה. (ראו גם טיפ מספר 137, 147)

לאחר מכן אנחנו מפעילים את הפונקציה עם הפרמטרים הנ"ל (ראו גם טיפ מספר 148)

מה הסוד כאן? הסוד הוא שהפונקציה GetMethod יודעת למצוא את הOverload שהכי מתאים לטיפוסים שאנחנו נותנים לה, בדומה לקומפיילר שבזמן קימפול מוצא את הOverload עם טיפוסי הפרמטרים הכי מתאימים.

מה זאת אומרת? אם, למשל, תהיה לנו את ההיררכיה הבאה:

1
Circle : Ellipse : Shape

ויהיו לנו רק את הOverloadים הבאים:

1
2
public static void InscribeInShape(Square innerShape, Ellipse circumscribedShape)
public static void InscribeInShape(Ellipse innerShape, Square circumscribedShape)

אז הקוד הבא:

1
2
3
Shape someShape = new Circle();
Shape someOtherShape = new Square();
InscribeInShape(someShape, someOtherShape);

יכנס לOverload

1
public static void InscribeInShape(Ellipse innerShape, Square circumscribedShape)

למרות הType הקונקרטי של someShape הוא Circle, ולא Ellipse.

זה די מגניב!


יש שפות שבהן שימוש בMultiple dispatch הוא יותר פשוט.

בשפת C# 4.0 נכנסה תמיכה לתוך השפה, כך שהרבה יותר קל להשתמש בזה.

הכלי הזה הוא חזק ומכליל את המושג של פונקציות וירטואליות שהתרגלנו להשתמש בו כבר.

ראו גם ב Wiki.

המשך יום דינאמי מרובה טוב!

שתף

223. Template method pattern

הכרנו קצת מהן פונקציות וירטואליות.

יישום מאוד שימושי וחזק של פונקציות וירטואליות הוא כתיבת Template methods.

Template method זו בעצם מתודה רגילה, הקוראת למתודות אחרות של האובייקט שלנו.

הTwist פה הוא שחלק מהמתודות אותן הTemplate method מפעילה, הן מתודות וירטואליות.

לדוגמה, נניח שיש לנו מחלקה כזאת:

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class DrinkMachine
{
protected abstract int GetProductAmount(string productId);
protected abstract void SetProductAmount(string productId, int amount);
protected abstract int GetProductPrice(string productId);
protected abstract bool CanCharge(int price);
protected abstract void Charge(int price);
protected abstract int ReturnChange();
}

זו מחלקה אבסטרקטית (עם עיצוב נאיבי) המייצגת מכונת שתייה. יש פה הרבה פונקציות שצריך לממש.

כעת נוכל לכתוב בעזרת פונקציות אלה מימוש לקנייה של מוצר:

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
public void BuyDrink(string productId)
{
int productsAvailable = GetProductAmount(productId);
if (productsAvailable == 0)
{
throw new ArgumentException("productId",
"No products of type " + productId + " are available");
}
int productPrice = GetProductPrice(productId);
if (!CanCharge(productPrice))
{
throw new Exception("Not enough money available. Excepted at least " + productPrice);
}
else
{
Charge(productPrice);
SetProductAmount(productId, productsAvailable - 1);
ReturnChange();
}
}

מה שיפה פה זה שלמרות המימוש של הפונקציות האלה טרם נכתב, אנחנו כן יכולים לכתוב שלד לפונקציה, המבצע את המבוקש.

כעת מי שירש מהמחלקה שלנו, יקבל אוטומטית מימוש לפונקציה BuyDrink.

הדבר הזה מאוד חזק.

הוא מאפשר מספר דברים טובים:

  1. מניעת שכפול קוד – במקום שכל אחד יממש בעצמו את הפונקציה הזאת, היא ממומשת פעם אחת בBase.
  2. שינוי התנהגות של המחלקה ע"י ירושה ודריסה.
  3. נקודות כניסה מוגבלות – במקום שהTemplate Method יהיה וירטואלי וכל אחד ידרוס אותו, אנחנו יכולים להגביל אותו, ולאפשר לדרוס רק את הפונקציות שהוא משתמש בהן.

המשך טיוטת יום וירטואלי טוב.

שתף

222. Virtual method calls in constructors

הכרנו פעם שעברה את המושג של מתודה וירטואלית, המאפשרת לאפליקציה שלנו להחליט בזמן ריצה לאיזו פונקציה לקרוא.

טעות נפוצה שיכולה להיווצר משימוש בפונקציות וירטואליות, היא קריאה להן בConstructor.

למה הכוונה?

הנה דוגמה קלאסית (ושכיחה!): נניח שיש לנו איזושהי פונקציה שמאתחלת לנו את אחד הMemberים של המחלקה. למשל, פונקציה שמאתחלת את הLogger של המחלקה שלנו:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Animal
{
private ILogger mLog;
public Animal()
{
this.mLog = GetLogger();
}
private ILogger GetLogger()
{
return new FileLogger();
}
}

כעת, אנחנו מעוניינים שמי שיורש מהמחלקה שלנו יוכל להחליף את האתחול של הLogger בפונקציה משלו.

הדבר הראשון שעולה לנו לראש היא להפוך את הפונקציהGetLogger לprotected וvirtual:

1
2
3
4
protected virtual ILogger GetLogger()
{
return new FileLogger();
}

יופי, עכשיו כל אחד יכול לדרוס את הפונקציה הזאת ולעשות איתה מה שהוא רוצה, למשל:

1
2
3
4
5
6
7
public class Dog : Animal
{
protected override ILogger GetLogger()
{
return new BoneLogger();
}
}

נראה שהפתרון טוב, אלא שיש פה בעיה.

מה הבעיה? נניח שלמחלקה היורשת יש איזשהו פרמטר משלה, והלוגר מאותחל לפיו:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Dog : Animal
{
private string mBark;
public Dog(string bark)
{
mBark = bark;
}
protected override ILogger GetLogger()
{
return new 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
public class Animal
{
private ILogger mLog;
public Animal(ILogger logger)
{
this.mLog = logger;
}
public Animal() : this(new FileLogger())
{
}
}
public class Dog : Animal
{
private string mBark;
public Dog(string bark): base(GetLogger(bark))
{
mBark = bark;
}
private static ILoggerGetLogger(string bark)
{
return new 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
public class Animal
{
private ILogger mLog;
public Animal()
{
}
protected virtual void Init()
{
this.mLog = GetLogger();
}
protected virtual ILogger GetLogger()
{
return new FileLogger();
}
}

ואז לאתחל את האובייקטים שלנו עם איזשהו Factory:

1
2
3
4
public interface IAnimalFactory
{
TAnimal GetAnimal<TAnimal>() where TAnimal : Animal, new();
}

עם מימוש כזה:

1
2
3
4
5
6
7
8
9
private class AnimalFactory : 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ים.

יום וירטואלי (אבל שלא נקרא מהConstructor) טוב!

שתף

221. virtual methods

בC# כמו בשפות תכנות רבות וטובות, קיים משהו שנקרא single dynamic dispatch.

השם היותר מוכר הוא מתודה וירטואלית – הFeature הזה מאפשר לנו להחליט לאיזו פונקציה לקרוא בזמן ריצה.

מה זה אומר?

כאשר הקוד שלנו מתקמפל, הקומפיילר צריך לדעת לאן לנתב את הקריאות לפונקציות שלנו.

יש פונקציות שכבר בזמן קימפול, הקומפיילר יודע את הכתובת שאליה הוא צריך לגשת כדי לקרוא להן, למשל הפונקציה הבאה:

1
2
3
4
5
6
7
public class MyClass
{
public int Add(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
public class Shape
{
public virtual void Draw()
{
// ...
}
}
public class Ellipse : Shape
{
public override void Draw()
{
Console.WriteLine("A very cool ellipse");
}
}
public class Circle : Ellipse
{
public override void Draw()
{
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.

שבוע וירטואלי טוב

שתף