240. DynamicXmlObject

שימוש נחמד נוסף בDynamicObject הוא יצירת Accessor נוח יותר לעבודה מול Xmlים.

איך זה עובד:

נירש מDynamicObject. אנחנו בעצם נעטוף Xml:

1
2
3
4
5
6
7
8
9
public class DynamicXmlObject : DynamicObject
{
private readonly XElement mElement;
public DynamicXmlObject(XElement element)
{
mElement = element;
}
}

כעת את הפונקציה TryGetMember נממש ע"י גישה לXml:

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
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
string propertyName = binder.Name;
XAttribute attribute = mElement.Attribute(propertyName);
if (attribute != null)
{
result = attribute.Value;
return true;
}
XElement element = mElement.Element(propertyName);
if (element != null)
{
if (!element.HasElements)
{
result = element.Value;
}
else
{
result = new DynamicXmlObject(element);
}
return true;
}
return base.TryGetMember(binder, out result);
}

כעת אם יש לנו Xml נוכל לגשת לProperties שלו בצורה נוחה יותר:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
XElement myXml =
new XElement("Person",
new XElement("Name", "Yossi"),
new XElement("Age", 31),
new XElement("LastName", "Levi"),
new XElement("Address",
new XAttribute("City", "Tel Aviv"),
new XElement("Street", "Namir Road")));
dynamic person = new DynamicXmlObject(myXml);
string age = person.Age; // 31
string lastName = person.LastName; // Levi
dynamic address = person.Address;
string city = address.City; // Tel Aviv

יש פה חסרון כי המתודות של XElement וXAttribute מחזירות stringים ולכן תמיד נקבל stringים.. לא מצאתי דרך לפתור את זה. (אם מישהו מוצא, אני אשמח לשמוע)

נוכל לעשות אותו הדבר גם להשמה של Properties:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public override bool TrySetMember(SetMemberBinder binder, object value)
{
string propertyName = binder.Name;
if (!(value is DynamicXmlObject))
{
mElement.SetElementValue(propertyName, value);
return true;
}
else
{
XElement element =
((DynamicXmlObject) value).mElement;
element.Name = propertyName;
mElement.Add(element);
return true;
}
return base.TrySetMember(binder, value);
}

ואז בעצם נוכל לכתוב את מה שכתבנו קודם כך:

1
2
3
4
5
6
7
person.Age = 31;
person.FirstName = "Yossi";
person.LastName = "Levi";
dynamic address = new DynamicXmlObject(newXElement("Address"));
address.City = "Tel Aviv";
address.Street = "Namir Road";
person.Address = address;

שיוצר את הXml האלגנטי:

1
2
3
4
5
6
7
8
9
<Person>
<Age>31</Age>
<FirstName>Yossi</FirstName>
<LastName>Levi</LastName>
<Address>
<City>Tel Aviv</City>
<Street>Namir Road</Street>
</Address>
</Person>

כעקרון זוהי הדגמה של מה אפשר לעשות עםDynamicObject.

זה לא בהכרח הBest Practice, וקיימים כבר פתרונות מאוד טובים לבעיה של הפיכת Xml לאובייקט, ביניהם Xsd וכו’.

אבל זה מגניב, ויכול להתאים למספר מקומות…

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

שתף

239. ExpandoObject

בהמשך למה שראינו פעם שעברה, קיים מימוש נחמד ב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)
{
Console.WriteLine("Property {0}: {1}",
propertyNameToValue.Key,
propertyNameToValue.Value);
}
//Property Age : 32
//Property LastName : Bond
//Property NickName : James Bond
//Property Job : Spy

מגניב, אה?

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

שתף

238. IDynamicMetaObjectProvider DynamicObject

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

לFramework יש תמיכה בשינוי ההתנהגות של מה שקורה כשאנחנו קוראים לפונקציה בצורה דינאמית ע”י שימוש בdynamic.

הדבר הזה נעשה ע”י מימוש הממשק ששמו IDynamicMetaObjectProvider

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyDynamicObject : DynamicObject
{
public override bool TryInvokeMember(InvokeMemberBinder binder,
object[] args,
out object result)
{
Console.WriteLine("{0} was called", binder.Name);
Console.WriteLine(args[0]);
return base.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) $. אל תבזבזו על זה שעה מהמבחן שלכם במידה וכתוב אחרת על הטופס.

שתף

237. Enum Dictionary

[מבוסס על הפוסט הזה]

אחד הדברים הטובים שקיבלנו ב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>
{
public bool Equals(Days x, Days y)
{
return (x == y);
}
public int GetHashCode(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
{
public bool Equals(TEnum x, TEnum y)
{
return (x == y);
}
public int GetHashCode(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;
public bool Equals(TEnum x, TEnum y)
{
return mEqualsMethod(x, y);
}
public int GetHashCode(TEnum obj)
{
return mGetHashCode(obj);
}
}

עכשיו נותר רק לאתחל את mEqualsMethod ואת mGetHashCode בזמן ריצה.

את זה נעשה בעזרת יצירת מתודות דינאמיות מתאימות בזמן ריצה ע"י שימוש בExpression Trees:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void InitEqualsMethod()
{
ParameterExpression parameterX =
Expression.Parameter(typeof(TEnum),"x");
ParameterExpression parameterY =
Expression.Parameter(typeof(TEnum),"y");
BinaryExpression compare =
Expression.Equal(parameterX, parameterY);
Expression<Func<TEnum, TEnum, bool>> lambdaExpression =
Expression.Lambda<Func<TEnum, TEnum, bool>>
(compare, parameterX, parameterY);
mEqualsMethod = lambdaExpression.Compile();
}

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

בקשר למימוש של GetHashCode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void InitGetHashCodeMethod()
{
ParameterExpression parameterX =
Expression.Parameter(typeof(TEnum),"x");
Type underlyingType = Enum.GetUnderlyingType(typeof(TEnum));
Expression convertExpression =
Expression.Convert(parameterX, underlyingType);
Expression getHashCode =
Expression.Call(convertExpression,
"GetHashCode",
Type.EmptyTypes);
Expression<Func<TEnum, int>> lambdaExpression =
Expression.Lambda<Func<TEnum,int>>
(getHashCode, parameterX);
mGetHashCode = lambdaExpression.Compile();
}

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

כעת נשאר לקרוא לאלה בConstructor:

1
2
3
4
5
public EnumEqualityComparer()
{
InitEqualsMethod();
InitGetHashCodeMethod();
}

ומומלץ גם ליצור מחלקה סטטית גנרית שתכיל את הEqualityComparerים האלה, כך שהם יווצרו רק פעם אחת לEnum:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static class EnumComparer<TEnum>
where TEnum : struct, IConvertible
{
private static IEqualityComparer<TEnum> mEqualityComparer =
new EnumEqualityComparer<TEnum>();
public static IEqualityComparer<TEnum> Comparer
{
get
{
return mEqualityComparer;
}
}
}

זהו, אפשר גם ליצור פונקציה שתיצור לנו Enum שהKey שלו הוא Enum נתון:

1
2
3
4
5
6
7
8
public static class EnumDictionary
{
public static IDictionary<TKey, TValue> Create<TKey, TValue>()
where TKey : struct, IConvertible
{
return new Dictionary<TKey,TValue>(EnumComparer<TKey>.Comparer);
}
}

ולקרוא לה כדי לקבל Dictionary בלי boxing בגישה אליו:

1
2
IDictionary<Days, string> noBoxingDictionary =
EnumDictionary.Create<Days,string>();

המשך יום דינאמי גנרי חסר קופסאות טוב

שתף

236. Shift Left, Shift Right operators

שני האופרטורים שזוכים לפחות קרדיט בC# הם האופרטורים >>, <<.

אופרטורים אלה מוכרים לנו משפת C והם בעצם מבצעים פעולות של Shift Left וShift Right.

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

1
2
3
int five = 5; // 101
int twenty = five << 2; // 10100
int two = five >> 1; // 10

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

למשל בדוגמה הראשונה, התוצאה שקולה להכפלה ב2 בחזקת 2, כלומר ב4.

בדוגמה השנייה, התוצאה שקולה לחילוק ב2 בחזקת 1, כלומר בחילוק ב2.

אפשר לטעון שאופרטורים אלה לא שימושיים ולכן אינם מוכרים.

אלא שיש שימוש נחמד בהם. זוכרים את הטיפ על FlagsAttribute?

בטיפ זה ציינתי שכדי ליצור Enum של Flags צריך לדאוג ידנית שהערכים שלו יהיו חזקות של 2.

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

במקום ככה:

1
2
3
4
5
6
7
8
[Flags]
public enum Directions
{
North = 1,
South = 2,
East = 4,
West = 8
}

אפשר לכתוב ככה:

1
2
3
4
5
6
7
8
[Flags]
public enum Directions
{
North = 1 << 0,
South = 1 << 1,
East = 1 << 2,
West = 1 << 3
}

התוצאה היא אותה תוצאה, רק שזה יוצא טיפה יותר אלגנטי.

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

1
2
3
4
5
6
7
8
9
10
[Flags]
public enum Directions
{
North = 1 << 0,
South = 1 << 1,
East = 1 << 2,
West = 1 << 3,
NorthEast = North | East,
SouthEast = South | East
}

די יפה.

שבוע עם שיפט לשמאל טוב.

שתף

235. Another variation of the visitor pattern

בהמשך לפעם שעברה,

נוכל להפוך את הVisitor שלנו לטיפה יותר גנרי:

1
2
3
4
5
6
7
8
9
public abstract class CarVisitor<T>
{
public T Visit(CarElement element)
{
return InnerVisit((dynamic) element);
}
protected abstract T InnerVisit(CarElement 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
public abstract class CarVisitor<T>
{
public T Visit(CarElement element)
{
return InnerVisit((dynamic)element);
}
protected abstract T InnerVisit(CarElement element);
protected virtual T InnerVisit(Wheel wheel)
{
return InnerVisit((CarElement)wheel);
}
protected virtual T InnerVisit(Door door)
{
return InnerVisit((CarElement)door);
}
protected virtual T InnerVisit(Car car)
{
return InnerVisit((CarElement)car);
}
}

עכשיו אנחנו נהנים מהיתרונות הבאים:

אפשר לדרוס את הפונקציות עבור טיפוס ספציפי (Wheel/Door/Car), אבל גם אם לא מממשים אותם, נקרא המימוש הדיפולטי.

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

בעצם אנחנו מנצלים את הDynamic Binding בשביל שינתב אותנו למתודה המתאימה.

יתרון נוסף של המימוש הזה הוא שהוא מאפשר לנו לקרוא ל"base" בצורה פשוטה יותר:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyVisitor : CarVisitor<string>
{
protected override string InnerVisit(CarElement element)
{
return element.GetType().Name;
}
protected override string InnerVisit(Wheel wheel)
{
return base.InnerVisit(wheel) + " With a diameter of " +
wheel.Diameter;
}
}

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

1
2
3
4
5
6
7
8
9
10
private string InnerVisit(CarElement element)
{
return element.GetType().Name;
}
private string InnerVisit(Wheel wheel)
{
return InnerVisit((CarElement)wheel) + " With a diameter of " +
wheel.Diameter;
}

שזה פחות טבעי.

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

שתף

234. Simplifying the visitor pattern

הכרנו בפעמים הקודמות את הDesign Pattern ששמו Visitor.

בשני המימושים שראינו היו שתי מחלקות:

הVisitor, עם הפונקציה שמקבלת את הElement, האחראי על הפעולה שאנחנו רוצים לבצע

1
2
3
4
public interface ICarElementVisitor
{
void Visit(CarElement element);
}

והElement, עם הפונקציה שמקבלת את הVisitor, המספר לנו איך לטייל עליו

1
2
3
4
public abstract class CarElement
{
public abstract void Accept(ICarElementVisitor carElementVisitor);
}

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

מצד שני, זה קצת מגביל אותנו. אם המחלקות של הElementים הן לא שלנו, ואנחנו רוצים "להוסיף" להן פונקציה וירטואלית, די נתקשה להוסיף את הפונקציה Visit אליהן.


מה אפשר לעשות? נניח לרגע שאנחנו מוותרים על האחריות של הElement לספר לנו איך לטייל עליו.

נניח שהמחלקות שיורשות מCarElement הן לא שלנו, ואנחנו מעוניינים לעשות משהו כזה: אנחנו מעוניינים לכתוב פונקציה בשםToXElement היוצרת מהInstanceים איזשהו Xml המתאר אותן.

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

ניצור מחלקת Visitor כמו פעם קודמת:

1
2
3
4
5
6
public class CarXElementVisitor
{
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)
{
return new XElement("Wheel", new XAttribute("Diameter",
wheel.Diameter));
}
private XElement InnerToXElement(Door door)
{
return new XElement("Door", new XAttribute("Width",
door.Width));
}
private XElement InnerToXElement(Car car)
{
return new XElement("Car",
new XAttribute("Company", car.Company),
new XAttribute("Color", car.Color),
car.Elements.Select(x => ToXElement(x)));
}
private XElement InnerToXElement(CarElement element)
{
return new XElement("UnkownElement",
new XAttribute("Type",
element.GetType().Name));
}

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

אם למשל נכתוב את הקוד הבא, נקבל את הפלט:

1
2
3
4
5
6
7
8
9
10
11
12
CarXElementVisitor visitor = new CarXElementVisitor();
Car car = new Car() { Color = "Red", Company = "Ford" };
car.Elements = new CarElement[]
{
new Wheel() { Diameter = 10 },
new Door() { Width = 23 }
};
XElement result = visitor.ToXElement(car);
//<Car Company="Ford" Color="Red">
// <Wheel Diameter="10" />
// <Door Width="23" />
//</Car>

שזה די מגניב.

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

עם זאת הפסדנו משהו. אם במימוש הקודם הVisitor לא היה צריך להכיר את המבנה של האובייקט שלנו, עכשיו הוא צריך. (הדבר מתבטא בInnerToXElement של Car).

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

יום דינאמי טוב

שתף

233. Making the visitor pattern dynamic

בהמשך לפעם הקודמת, הכרנו קצת את ה Design Pattern ששמוVisitor .

תזכורת: יוצרים איזשהו Class כזה:

1
2
3
4
5
6
7
8
public interface ICarElementVisitor
{
void Visit(Wheel wheel);
void Visit(Door door);
void Visit(Engine engine);
void Visit(Body body);
void Visit(Car car);
}

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

1
public abstract void Accept(ICarElementVisitor visitor);

שמספרת לVisitor איך הוא אמור לטייל עלינו.

הבעיה העיקרית במימוש הזה הוא שICarElementVisitor סותר את עקרונות הOOP.

הסיבה לכך היא שהממשק ICarElementVisitorמכיר כבר את כל הטיפוסים היורשים מCarElement, ולכן לא מאפשר לנו הרחבה למימושים אחרים של CarElement.


איך אפשר לתקן?

ראשית נדאג שהממשק ICarElementVisitor יקבל רק את הטיפוס הבסיסי:

1
2
3
4
public interface ICarElementVisitor
{
void Visit(CarElement element);
}

כעת המימוש של הVisitor שלנו ישתמש בDynamic Binding כדי למצוא את הOverload המתאים ביותר:

1
2
3
4
public void Visit(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
public class CarPhotoShooter : ICarElementVisitor
{
public void Visit(CarElement element)
{
InnerVisit((dynamic)element);
}
private void InnerVisit(Wheel wheel)
{
Console.WriteLine("Taking a photo of the wheel");
}
private void InnerVisit(Door door)
{
Console.WriteLine("Taking a photo of the door");
}
private void InnerVisit(Car car)
{
Console.WriteLine("What a nice car!");
}
// ...
private void InnerVisit(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();
Car car = new Car();
car.Accept(visitor);

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

שתף

232. Visitor design pattern

אחד הDesign patterns החזקים שקיימים נקרא Visitor.

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

הדבר הזה גם מאפשר לנו “להוסיף” מתודה וירטואלית למשפחה של טיפוסים, מבלי לשנות את הקוד שלהם.

איך זה עובד?

נניח שיש לנו את המשפחה הבאה של טיפוסים:

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class CarElement
public class Wheel : CarElement
public class Door : CarElement
public class Engine : CarElement
public class Body : CarElement
public class Car : CarElement
{
public IEnumerable<CarElement> Elements
{
get;
private set;
}
}

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

בלי שימוש בVisitor, השיטה היא להוסיף פונקציה וירטואלית בCarElement:

1
2
3
4
public abstract class CarElement
{
public abstract void ShootPhoto();
}

ולדאוג לממש אותה בכל מחלקת בת.

יש שתי בעיות בגישה הזאת:

הבעיה הראשונה היא שעל כל פעולה שאנחנו מעוניינים להוסיף, אנחנו נצטרך להוסיף פונקציה וירטואלית כזאת. הבעיה עם זה היא שזה יכול לנפח את המחלקות שלנו. מה שיכול לקרות זה שCarElement "יממש" את הAnti-pattern שנקרא God Class, שהוא נהיה ענק ועמוס בפונקציות, כך שאנחנו לא כל כך רוצים להוסיף אליו עוד פונקציות.

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


כדי לפתור את בעיות אלו, אפשר להשתמש בDesign Pattern ששמו Visitor.

לפי Design pattern זה, יש רק פונקציה אבסטרקטית אחת שצריך לממש:

1
2
3
4
public abstract class CarElement
{
public abstract void Accept(ICarElementVisitor visitor);
}

כאשר הממשק ICarElementVisitor נראה ככה:

1
2
3
4
5
6
7
8
public interface ICarElementVisitor
{
void Visit(Wheel wheel);
void Visit(Door door);
void Visit(Engine engine);
void Visit(Body body);
void Visit(Car car);
}

המימוש בכל אחת מהמחלקות בנות מתחשב במבנה של המחלקה:

למשל:

1
2
3
4
5
6
7
public class Wheel : CarElement
{
public override void Accept(ICarElementVisitor visitor)
{
visitor.Visit(this);
}
}

ואילו עבור מכונית:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Car : CarElement
{
public IEnumerable<CarElement> Elements
{
get;
private set;
}
public override void Accept(ICarElementVisitor visitor)
{
foreach (CarElement element in Elements)
{
element.Accept(visitor);
}
visitor.Visit(this);
}
}

הדבר הזה מאפשר לנו "להוסיף" פונקציות וירטואליות מבחוץ: אנחנו פשוט צריכים לממש אתICarElementVisitor ולעשות בו כאוות נפשנו.

למשל:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CarPhotoShooter : ICarElementVisitor
{
public void Visit(Wheel wheel)
{
Console.WriteLine("Taking a photo of the wheel");
}
public void Visit(Door door)
{
Console.WriteLine("Taking a photo of the door");
}
// ...
public void Visit(Car wheel)
{
Console.WriteLine("What a nice car!");
}
}

ואז נוכל לקרוא לפונקציה זו כך:

1
2
3
ICarElementVisitor visitor = newCarPhotoShooter();
Car car = new Car();
car.Accept(visitor);

נוכל אפילו לקרוא לפונקציה זו כאילו היא הייתה פונקציה וירטואלית רגילה, ע"י שימוש בExtension Methods 😃

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

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

שתף

231. Calling generic methods with unknown generic type, strikes back

בעבר הרחוק (טיפ מספר 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)
{
}
}

פתרון זה בעייתי ממספר סיבות:

  1. הוא ממש מכוער – אנחנו מוודאים האם הפונקציה קיימת או לא ע"י תפיסת Exception.
  2. הוא שגוי לוגית – בשום מקום אנחנו לא מוודאים שהטיפוס מממש את ICloneable<T>, כך שאם יש לאובייקט שאנחנו מריצים עליו את הפונקציה פונקציה Clone אחרת, עדיין היא תכנס לתוצאה.
  3. הפתרון הזה יכול לא לעבוד אפילו אם הטיפוס שלנו מממש ICloneable<T>. מה זאת אומרת? כשדיברנו על Dynamic Binding לראשונה, ציינתי שDynamic Binding יודע למצוא רק מתודות שהן public. ובכן, אם הטיפוס שלנו מממש את הממשק, אבל באופן Explicitly, יזרק לנו Exception, ולכן הוא לא עושה את מה שרצינו. (ראו גם טיפ מספר 82)

אז מה אפשר לעשות?

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

נוכל לנצל עובדה זו כדי לפתור את הבעיה הנ"ל:

נכתוב פונקציה גנרית כזאת:

1
2
3
4
private static 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
private static object Clone(object source)
{
return null;
}

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

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

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

שתף