אחד הדברים הטובים שקיבלנו ב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: