בהמשך למה שראינו פעם שעברה, קיים מימוש נחמד בFramework שלIDynamicMetaObjectProvider.
המימוש הנחמד נקרא ExpandoObject – זהו אובייקט שכפי שמרמז שמו, ניתן להרחבה.
איך משתמשים בזה? ככה:
1
2
3
4
5
6
7
8
ExpandoObject expando = new ExpandoObject();
dynamic dynamicExpando = expando;
dynamicExpando.Age = 32;
dynamicExpando.LastName = "Bond";
dynamicExpando.NickName = "James Bond";
dynamicExpando.Job = "Spy";
Console.WriteLine(dynamicExpando.Age); // 32
Console.WriteLine(dynamicExpando.NickName); // James Bond
הקוד הזה מתקמפל ועובד!
מה קורה כאן?
כל פעם שאנחנו ניגשים לSetter של Property, נוצר לאותו Property איזשהו Storage מאחורי הקלעים.
איך זה עובד?
תכלס הדבר הזה ממומש באמצעות Dictionary של שמות שלProperties לערכים שלהם. באופן ממש לא מפתיע, הוא גם מממש IDictionary<string, object>, כך שאנחנו יכולים לרוץ על ערכי הExpandoObject:
1
2
3
4
5
6
7
8
9
10
11
foreach (KeyValuePair<string,object> propertyNameToValue in expando)
בפעמים הקודמות הכרנו קצת את השימוש בdynamic המאפשר לנו להפעיל מתודות על האובייקטים שלנו באופן דינאמי.
לFramework יש תמיכה בשינוי ההתנהגות של מה שקורה כשאנחנו קוראים לפונקציה בצורה דינאמית ע”י שימוש בdynamic.
הדבר הזה נעשה ע”י מימוש הממשק ששמו IDynamicMetaObjectProvider
מאחר וקצת קשה לממש את הממשק הזה, יש מחלקת אב בשם DynamicObject שאנחנו יכולים לרשת ממנה ולהחליט על ההתנהגות של האובייקטים שלנו ברגע שמנסים להפעיל עליו מתודות בצורה דינאמית.
לדוגמה נוכל לרשת מDynamicObject ולדרוס את הפונקציה TryInvokeMember. זו פונקציה שנקראת כל פעם שאנחנו מנסים לקרוא לפונקציה של האובייקט בצורה דינאמית:
returnbase.TryInvokeMember(binder, args, out result);
}
}
למשל, כשנכתוב את הקוד הבא:
1
2
dynamic sample = new MyDynamicObject();
sample.MyMethod("This compiles but throws an exception");
נקבל את התוצאה הבאה:
MyMethod was called This compiles but throws an exception
זה קורה כי הדפסנו את הפרמטר הראשון ואת השם של הפונקציה. עף Exception כי קראנו לbase וזה מנסה לקרוא לפונקציה בצורה דינאמית סטנדרטית (כמו שראינו בטיפים הראשונים על dynamic).
המשך יום דינאמי טוב,
כי פונקציה זוגית סביב $ \mu $ זו פונקציה שמקיימת $ f(\mu - x) = f(x + \mu) $ ולא $ f(x-\mu) = f(x + \mu) $. אל תבזבזו על זה שעה מהמבחן שלכם במידה וכתוב אחרת על הטופס.
אחד הדברים הטובים שקיבלנו בFramework 2.0 הוא Generics.
בין השאר, אחד הדברים שזה מאפשר לנו, בניגוד לאובייקטים בFramework 1.0 הוא להמנע מBoxing.
אז אולי יפתיע אתכם לשמוע ששימוש בDictionary שהמפתח שלו הוא Enum גורר לBoxing בכל גישה לDictionary.
איך זה יכול לקרות?
בעבר הרחוק הכרנו את המתודה Equals.
ראינו שהFramework משתמש בה בכדי לבצע השוואות במתודות חשובות.
בין השאר, כאשר מתבצעת גישה לDictionary עפ”י Key, מתבצע חיפוש של הKey בעזרת הHashCode שלו (לפי הפונקציה GetHashCode) ולאחר מכן מתבצע חיפוש שלו ע”י השוואה שלו עם כל Key אחר בעזרת הפונקציה Equals שלEqualityComparer.Default. (זאת במידה ואנחנו לא מעבירים לDictionary בConstructor שלו EqualityComparer משלנו)
עד כאן הכל טוב ויפה. בטיפ על EqualityComparer.Default ציינתי שפונקציית הEquals שלו יודעת להמנע מBoxing בתנאי שTמממש IEquatable.
אלא שאם נסתכל על Enum שניצור, נראה שהוא לא מממש IEquatable ולכן כאשר מתבצעת גישה לDictionary, בסופו של דבר נקראת הפונקציה Equals שמקבלת object.
הדבר גורר boxing בקריאה למתודה, שמוביל לביצועים גרועים בגישה לDictionary, במיוחד אם ניגשים אליו מספר רב של פעמים.
מה אפשר לעשות?
הדבר הראשון שאפשר לעשות הוא לממש IEqualityComparer משלנו שישווה את הEnum בצורה נכונה:
1
2
3
4
5
6
7
8
9
10
11
12
public class DaysEqualityComparer : IEqualityComparer<Days>
{
publicboolEquals(Days x, Days y)
{
return (x == y);
}
publicintGetHashCode(Days obj)
{
return (int)obj;
}
}
שימו לב שאמנם לEnum אין פונקציית Equals שמשווה ערכים בלי Boxing, אבל מממומש האופרטור == שמשווה את הEnum בלי boxing.
המימוש הוא למעשה די פשוט, ונוכל להעביר אותו לכל Dictionary שניצור שDays המפתח שלו:
1
2
Dictionary<Days, string> noBoxingDictionary =
new Dictionary<Days, string>(new DaysEqualityComparer());
זה טוב, אבל זה מכריח אותנו לכתוב בערך אותה מחלקה עבור כל סוג Enum שאנחנו רוצים שיהיה מפתח בDictionary.
מה שמתבקש כאן זה להשתמש בGenerics:
1
2
3
4
5
6
7
8
9
10
11
12
13
public class EnumEqualityComparer<TEnum> : IEqualityComparer<TEnum>
where TEnum : struct, IConvertible
{
publicboolEquals(TEnum x, TEnum y)
{
return (x == y);
}
publicintGetHashCode(TEnum obj)
{
return (int)obj;
}
}
אלא שלמרבה הצער זה לא מתקמפל – הקומפיילר לא יודע לזהות בזמן קימפול האם קיים האופרטור == או אם קיימת הסבה לint.
מה שנוכל לעשות במקום זה לחולל את המחלקות האלה בזמן ריצה.
מה שאנחנו מעוניינים לעשות זה בעצם ליצור EqualityComparer שיודע לטפל בכל Enum.
נעשה משהו כזה:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class EnumEqualityComparer<TEnum> : IEqualityComparer<TEnum>
where TEnum : struct, IConvertible
{
private Func<TEnum, TEnum, bool> mEqualsMethod;
private Func<TEnum, int> mGetHashCode;
publicboolEquals(TEnum x, TEnum y)
{
return mEqualsMethod(x, y);
}
publicintGetHashCode(TEnum obj)
{
return mGetHashCode(obj);
}
}
עכשיו נותר רק לאתחל את mEqualsMethod ואת mGetHashCode בזמן ריצה.
את זה נעשה בעזרת יצירת מתודות דינאמיות מתאימות בזמן ריצה ע"י שימוש בExpression Trees:
מצד אחד זה בסדר, מאחר והפונקציה של הElement מספרת לנו איך לטייל עליו, ואילו הVisitor רק צריך לדעת איזו פעולה להפעיל.
מצד שני, זה קצת מגביל אותנו. אם המחלקות של הElementים הן לא שלנו, ואנחנו רוצים "להוסיף" להן פונקציה וירטואלית, די נתקשה להוסיף את הפונקציה Visit אליהן.
מה אפשר לעשות? נניח לרגע שאנחנו מוותרים על האחריות של הElement לספר לנו איך לטייל עליו.
נניח שהמחלקות שיורשות מCarElement הן לא שלנו, ואנחנו מעוניינים לעשות משהו כזה: אנחנו מעוניינים לכתוב פונקציה בשםToXElement היוצרת מהInstanceים איזשהו Xml המתאר אותן.
אז נוכל לעשות משהו כזה:
ניצור מחלקת Visitor כמו פעם קודמת:
1
2
3
4
5
6
publicclassCarXElementVisitor
{
public XElement ToXElement(CarElement element)
{
}
}
שתמומש כך:
1
2
3
4
public XElement ToXElement(CarElement element)
{
return InnerToXElement((dynamic) element);
}
כעת לכל טיפוס ניצור Overload מתאים:
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
private XElement InnerToXElement(Wheel wheel)
{
returnnew XElement("Wheel", new XAttribute("Diameter",
wheel.Diameter));
}
private XElement InnerToXElement(Door door)
{
returnnew XElement("Door", new XAttribute("Width",
הבעיה העיקרית במימוש הזה הוא שICarElementVisitor סותר את עקרונות הOOP.
הסיבה לכך היא שהממשק ICarElementVisitorמכיר כבר את כל הטיפוסים היורשים מCarElement, ולכן לא מאפשר לנו הרחבה למימושים אחרים של CarElement.
איך אפשר לתקן?
ראשית נדאג שהממשק ICarElementVisitor יקבל רק את הטיפוס הבסיסי:
1
2
3
4
publicinterfaceICarElementVisitor
{
voidVisit(CarElement element);
}
כעת המימוש של הVisitor שלנו ישתמש בDynamic Binding כדי למצוא את הOverload המתאים ביותר:
1
2
3
4
publicvoidVisit(CarElement element)
{
InnerVisit((dynamic)element);
}
כאשר לכל טיפוס יש InnerVisit משלו. אם ניקח את הדוגמה מפעם שעברה, כעת היא תמומש כך:
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
29
30
publicclassCarPhotoShooter : ICarElementVisitor
{
publicvoidVisit(CarElement element)
{
InnerVisit((dynamic)element);
}
privatevoidInnerVisit(Wheel wheel)
{
Console.WriteLine("Taking a photo of the wheel");
}
privatevoidInnerVisit(Door door)
{
Console.WriteLine("Taking a photo of the door");
}
privatevoidInnerVisit(Car car)
{
Console.WriteLine("What a nice car!");
}
// ...
privatevoidInnerVisit(CarElement element)
{
Console.WriteLine("Taking a photo of {0}. I don't really know how to shoot this.",
element.GetType());
}
}
מה שיקרה זה שבזמן ריצה נמצא הOverload המתאים ביותר, כמו שהיה נמצא בזמן קימפול – בטיפ של אתמול, מאחר ובממשק היו הרבה Overloadים, הוא ידע בזמן קימפול לנתב אותנו לOverload המתאים ביותר. עכשיו בזמן ריצה ננותב למתודה המתאימה ביותר.
הדבר הזה בעצם מאפשר לנו "להוסיף" מתודה וירטואלית ולדרוס אותה עבור כל מימוש שנרצה של CarElement, בניגוד לפעם שעברה, שאפשר לנו "להוסיף" מתודה וירטואלית רק לטיפוסים שאנחנו כבר מכירים. (זה לא מדויק, כי יכולנו לעשות כל מיני בדיקות של if (element is Mazda), אבל תסכימו איתי שזה פחות אלגנטי…)
השימוש, כמו בפעם הקודמת, הוא משהו כזה:
1
2
3
ICarElementVisitor visitor = new CarPhotoShooter();
הוא בגדול מאפשר לנו לבצע פעולות על משפחה של אובייקטים מבלי להכיר את המבנה הפנימי שלהם.
הדבר הזה גם מאפשר לנו “להוסיף” מתודה וירטואלית למשפחה של טיפוסים, מבלי לשנות את הקוד שלהם.
איך זה עובד?
נניח שיש לנו את המשפחה הבאה של טיפוסים:
1
2
3
4
5
6
7
8
9
10
11
12
13
publicabstractclassCarElement
publicclassWheel : CarElement
publicclassDoor : CarElement
publicclassEngine : CarElement
publicclassBody : CarElement
publicclassCar : CarElement
{
public IEnumerable<CarElement> Elements
{
get;
privateset;
}
}
כעת אנחנו מעוניינים להוסיף אפשרות של לצלם רכיב של המכונית.
בלי שימוש בVisitor, השיטה היא להוסיף פונקציה וירטואלית בCarElement:
1
2
3
4
publicabstractclassCarElement
{
publicabstractvoidShootPhoto();
}
ולדאוג לממש אותה בכל מחלקת בת.
יש שתי בעיות בגישה הזאת:
הבעיה הראשונה היא שעל כל פעולה שאנחנו מעוניינים להוסיף, אנחנו נצטרך להוסיף פונקציה וירטואלית כזאת. הבעיה עם זה היא שזה יכול לנפח את המחלקות שלנו. מה שיכול לקרות זה שCarElement "יממש" את הAnti-pattern שנקרא God Class, שהוא נהיה ענק ועמוס בפונקציות, כך שאנחנו לא כל כך רוצים להוסיף אליו עוד פונקציות.
הבעיה השנייה שאנחנו צריכים לדעת מהו המבנה עבור כל מחלקת בת. למה הכוונה? נניח במקום לצלם מכונית, אפשר לצלם פשוט את החלקים המרכיבים אותה. אנחנו צריכים להתחשב בזאת בדריסה במחלקה Car.
כדי לפתור את בעיות אלו, אפשר להשתמש בDesign Pattern ששמו Visitor.
לפי Design pattern זה, יש רק פונקציה אבסטרקטית אחת שצריך לממש:
בעבר הרחוק (טיפ מספר 150) דיברנו על כך שלפעמים נרצה לקרוא לפונקציה גנרית עם פרמטר גנרי לא ידוע.
זה מצב שנדיר להיתקל בו כאשר הטיפוסים שלנו מעוצבים היטב. למרבה הצער, לא תמיד הטיפוסים שלנו מעוצבים היטב.
תזכורת: יש לנו איזשהו ממשק גנרי:
1
2
3
4
public interface ICloneable<T>
{
T Clone();
}
כעת יש לנו אוסף של אובייקטים, חלקם מהסוג הזה (כל אחד עם פרמטר גנרי אחר), ואנחנו מעוניינים ליצור מהם אוסף חדש של אובייקטים ע"י הפעלת הפונקציה Clone על כל אחד מהם.
הדרך שלא עובדת:
1
2
3
4
5
6
7
8
9
10
ICollection<object> result = new List<object>();
foreach (object current in collection)
{
if (current is ICloneable<>)
{
ICloneable<> cloneable = (ICloneable<>) current;
result.Add(cloneable.Clone());
}
}
שתי השורות הראשונות בתוך הלולאה לא מתקמפלות.
ראינו דאז כיצד ניתן לפתור בעיה זו באמצעות Invoke של MethodInfo, אלא שזה דרש מאיתנו לכתוב די הרבה קוד.
כעת נראה איך אפשר לפתור בעיה זו בעזרת Dynamic Binding של C# 4.0.
הפתרון הראשון הוא כזה:
1
2
3
4
5
6
7
8
9
10
11
12
ICollection<object> result = new List<object>();
foreach (object current in collection)
{
try
{
dynamic dynamicCurrent = current;
result.Add(dynamicCurrent.Clone());
}
catch (RuntimeBinderException e)
{
}
}
פתרון זה בעייתי ממספר סיבות:
הוא ממש מכוער – אנחנו מוודאים האם הפונקציה קיימת או לא ע"י תפיסת Exception.
הוא שגוי לוגית – בשום מקום אנחנו לא מוודאים שהטיפוס מממש את ICloneable<T>, כך שאם יש לאובייקט שאנחנו מריצים עליו את הפונקציה פונקציה Clone אחרת, עדיין היא תכנס לתוצאה.
הפתרון הזה יכול לא לעבוד אפילו אם הטיפוס שלנו מממש ICloneable<T>. מה זאת אומרת? כשדיברנו על Dynamic Binding לראשונה, ציינתי שDynamic Binding יודע למצוא רק מתודות שהן public. ובכן, אם הטיפוס שלנו מממש את הממשק, אבל באופן Explicitly, יזרק לנו Exception, ולכן הוא לא עושה את מה שרצינו. (ראו גם טיפ מספר 82)
אז מה אפשר לעשות?
אם נזכר בטיפ הקודם, קריאה דינאמית לפונקציה עם פרמטר גנרי, מבצעת הזרקה בזמן ריצה לפרמטר הגנרי המתאים ביותר.
נוכל לנצל עובדה זו כדי לפתור את הבעיה הנ"ל:
נכתוב פונקציה גנרית כזאת:
1
2
3
4
privatestatic T Clone<T>(ICloneable<T> source)
{
return source.Clone();
}
ואז נכתוב את הקוד הבא:
1
2
3
4
5
6
7
8
9
10
11
12
13
ICollection<object> result = new List<object>();
foreach (object current in collection)
{
try
{
dynamic dynamicCurrent = current;
result.Add(Clone(dynamicCurrent));
}
catch (RuntimeBinderException e)
{
}
}
כעת התגברנו על בעיות 2 ו3 – לא נכנס לפונקציה Clone במידה ואנחנו לא מממשים את הממשק. במידה וכן, הInstance שלנו הוא כבר מסוג ICloneable<T> ולכן נוכל לקרוא לפונקציה של הממשק.
עם זאת לא התגברנו על הבעיה המעצבנת של תפיסת Exception.
נוכל לפתור בעיה זו בצורה אלגנטית ע"י הוספת פונקציה כזו:
1
2
3
4
privatestaticobjectClone(object source)
{
returnnull;
}
כעת נוכל לעשות משהו כזה:
1
2
3
4
5
6
7
8
9
10
11
12
ICollection<object> result = newList<object>();
foreach (object current in collection)
{
dynamic dynamicCurrent = current;
object cloneResult = Clone(dynamicCurrent);
if (cloneResult != null)
{
result.Add(cloneResult);
}
}
מה קורה כאן? כאשר אנחנו קוראים לClone, בזמן ריצה נבחר הOverload המתאים ביותר.
אם אנחנו מממשים את הממשק ICloneable<T>, יבחר הoverload הראשון. אחרת יבחר הoverload השני.
לכן אם אנחנו לא מממשים את הממשק ICloneable<T>, נקבל פשוט null.
זה לא פותר את הבעיה במידה ואנחנו מממשים את הממשק, והפונקציה Clone מחזירה null, אבל די פשוט להתגבר על המקרה הזה.