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>();

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

שתף