81. Type encapsulation

אחד הדברים המציקים שאני רואה לא מעט בזמן האחרון זה קוד מהצורה הבאה:

1
2
3
4
public List<MyClass> MyFunction()
{
// ...
}

או יותר גרוע:

1
2
3
4
public void MyFunction(List<MyClass> givenList)
{
// ...
}

הסבר:

הפונקציה הראשונה מחזירה List<MyClass>, הפונקציה השנייה מקבלת List<MyClass>.

מה הבעיה בזה?

במידה ואנחנו מחזירים List<MyClass>, בעצם אין פה שום אנקפסולציה של הType שאנחנו מחזירים.

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

הפתרון:

נחזיר IEnumerable<MyClass> במקום, ובמידה ואנחנו צריכים יותר מרק IEnumerable, נחזיר ICollection<MyClass> או אפילו IList<MyClass>.

במידה ואנחנו מחזירים את השניים האחרונים, ואנחנו לא מעוניינים שישנו לנו את הרשימה, נוכל להשתמש בReadOnlyCollection. (ראו גם טיפ יומי מספר 4)

בקשר לפונקציה השנייה, שם יש בעיה יותר גדולה:

הפונקציה מקבלת List<MyClass>.

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

לרוב אנחנו בכלל לא נצטרך את המבנה של List<MyClass> ונוכל להסתפק בIEnumerable<MyClass> (למטרות ריצה בלבד), או לכל היותר ICollection<MyClass> (למטרות יותר מורכבות, כגון שינוי האוסף, ספירת גודלו, בדיקה אם הוא מכיל איבר וכו’)

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


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

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

שבוע ממומשק טוב

שתף

80. implementing == operator

בהמשך לטיפים של השבוע,

ראינו מה זה Equals ואיך אמורים לממש אותו.

כעת נדבר קצת על האופרטור ==.

כאמור, זוהי מתודה סטטית, ולכן קריאה אליה היא יחסית מהירה.

בקשר למימוש הדיפולטי שלה:

עבור value types אין מימוש דיפולטי

עבור reference type המימוש הדיפולטי הוא השוואת referenceים.

קיימות שתי דרכים פופולאריות לממש אותו:

  1. להשאיר דיפולטי של השוואת Referenceים
  2. להחליף את המימוש בקריאה לEquals של האובייקט.

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

  1. בvalue types – מאחר ואחרת לא נוכל להשתמש באופטור
  2. בreference types – כאשר מדובר באובייקט immutable (שאינו בר שינוי), כיוון שאז שוויון של שני אובייקטים הוא שוויון קבוע. למשל, בstring מומש האופרטור == ע”י קריאה לEquals, גם אם שני הstringים אינם מצביעים לאותו reference.

שימו לב שמימוש של האופרטור ==, דורש גם מימוש של האופרטור =!.

ביכולתנו לממש אותו כאוות נפשנו. למרות זאת, נעדיף בד”כ לממש אותו בצורה הטריוויאלית:

1
2
3
4
public static bool operator !=(MyClass a, MyClass b)
{
return !(a == b);
}

אלא אם כן נרצה לעשות משהו מוזר.


אמרנו בתחילת השבוע שלא תמיד Equals ו== הם זהים.

נראה כעת מקרה בו מומשו גם Equals וגם ==, עם זאת המימוש שונה.

למחלקה double יש מימוש לEquals ול==. הם בד"כ מזדהים:

1
2
3
4
5
6
7
8
9
10
11
12
double first = 4;
double second = 4;
if (first == second)
{
// true
}
if (first.Equals(second))
{
// true
}

אבל יש מקרים יוצאי דופן:

1
2
3
4
5
6
7
8
9
10
11
12
double first = double.NaN;
double second = double.NaN;
if (first == second)
{
// false
}
if (first.Equals(second))
{
// true
}

ההיגיון אומר את הדבר הבא:

NaN (Not a number) הוא לא מספר, ולכן אינו זהה לאף מספר אחר

לכן גם אינו זהה לעצמו.

מצד שני, לשני NaNים שונים, יש אותו "ערך" ולכן הם שווים מבחינת Equals.

סופ"ש שווה!

שתף

79. Equatable.DefaultComparer

בהמשך לטיפ היומי של אתמול,

ראינו איך אפשר להשוות שני משתנים בEquals בלי לבדוק אם אחד מהם הוא null.

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

מסתבר שקיימת דרך דומה לבצע את ההשוואה בצורה הבאה:

במקום לכתוב ככה:

1
2
3
4
if (object.Equals(a, b))
{
// ...
}

נוכל לכתוב ככה:

1
2
3
4
if (EqualityComparer<int>.Default.Equals(a, b))
{
// ...
}

כאשר לEquals נוכל להעביר רק טיפוסים מהסוג הגנרי.

מה הרווחנו:

  1. Typed safety – אם ברצוננו להשוות רק משתנים מאותו טיפוס, זה יבדוק את זה.
  2. מניעה מboxing – בקריאה למתודה אין boxing
  3. התחשבות בIEquatable<T> - אם הטיפוסים שאנחנו משווים מממשים IEquatable<T>, המתודה הזאת תשתמש בEquals של הממשק במקום בEquals שלobject, ולכן נמנע מboxing במידה והממשק מממומש.
  4. ביצועים – מאחר וחסכנו boxing, הביצועים יהיו טובים יותר.

אני רוצה להוסיף ולציין מקרה שבו נרצה להשתמש במתודות Equals הסטטיות:

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

1
2
3
4
5
6
7
public static void MyMethod<T>(T first, T second)
{
if (first == second)
{
// Compile error.
}
}

במקום זאת נצטרך לעשות אחד מהשניים:

  1. להשתמש בהשוואת referenceים
  2. להשתמש בEquals

נוכל לכתוב כך:

1
2
3
4
5
6
7
public static void MyMethod<T>(T first, T second)
{
if (object.Equals(first, second))
{
// ...
}
}

או יותר טוב, כך:

1
2
3
4
5
6
7
public static void MyMethod<T>(T first, T second)
{
if (EqualityComparer<T>.Default.Equals(first, second))
{
// ...
}
}

המשך יום שווה טוב

שתף

78. object Equals

ראינו בפעם הקודמת מהו Equals ואיך מממשים אותו.

לצערנו, קריאה לEquals היא “פחות כיפית” מקריאה לאופרטור הסטטי ==.

אחד הדברים המעצבנים הוא שכאשר אנחנו קוראים לEquals, מאחר ומדובר במתודה של הinstance, היא שאנחנו צריכים לבדוק מה קורה אם הinstance הוא null.

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

1
2
3
4
5
6
7
8
9
10
11
public static bool AreEqual(object first, object second)
{
if (first == null)
{
return (second == null);
}
else
{
return (first.Equals(second));
}
}

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

(שימו לב שמאחר ומדובר בreferenceים מסוג object, ההשוואה עם null היא השוואה של referenceים)

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

כלומר נוכל לכתוב תמיד

1
2
3
4
if (Equals(first, second))
{
// ...
}

במקום כך:

1
2
3
4
5
// What if first is null???
if (first.Equals(second))
{
// ...
}

יום שווה טוב

שתף

77. Equals override

הכרנו אתמול מעט את Equals ואת האופרטור ==.

נדבר היום על דריסה של Equals.

ובכן, יש מספר סיבות שנרצה לדרוס את Equals:

הסיבה הראשונה היא כדי שתהיה התנהגות רצויה בשימוש במתודה Equals.

למשל, כאשר נממש מימוש של מספר רציונלי, נרצה שאם נשווה אותו לint, תהיה התחשבות אם המספר הרציונלי מייצג את אותו מספר שלם כמו הint.

סיבה נוספת, היא שימוש של הFramework – כאשר אנחנו משתמשים בהרבה מפונקציות הFramework מתבצעת קריאה לEquals.

לא חסרות דוגמאות, בין השאר קריאה לContains של List ומתודות נוספות.

אולי הדוגמה הכי חשובה לכך היא שימוש במחלקה בתור Key בDictionary. כדי למצוא את האיבר, הFramework משתמש בEquals.

לכן אם לא נממש את Equals, ייתכן ונקבל התנהגות לא רצויה בגישה לDictionary, ובפונקציות אחרות של הFramework. (ראו גם טיפ מספר 273)


המימוש הדיפולטי של Equals הוא כנ”ל:

עבור Reference Types, מתבצעת השוואת Referenceים

עבור Value Types מתבצעת השוואה של רכיב רכיב בעזרת הEquals של כל אחד מהם

לדוגמה:

1
2
3
4
5
public struct MyStruct
{
public int Number;
public string Name;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
MyStruct firstObject = new MyStruct();
firstObject.Name = "Hello";
firstObject.Number = 3;
MyStruct secondObject = new MyStruct();
secondObject.Name = "Hello";
secondObject.Number = 3;
if (firstObject.Equals(secondObject))
{
// True
}

הEquals יחזיר true, שהרי מושווים כל השדות של הstruct.

אם נחליף את המילה struct במילה class, הEquals יחזיר false, שהרי מדובר בReferenceים שונים.

שימו לב שזוהי עוד סיבה שייתכן שנרצה לדרוס את Equals.

מאחר ובValue Type מושווים כל השדות, המימוש יכול להיות איטי (במיוחד אם הוא מחזיק reference types שלהם הequals ממומש בצורה איטית). אולי נרצה להחליף אותו במימוש אחר מטעמי ביצועים.


למה אנחנו צריכים לדאוג כשאנחנו דורסים את Equals?

כדי שEquals תהיה פונקציה משמעותית ושימושית, היא צריכה לקיים את התנאים הבאים:

  1. x.Equals(x) מחזיר תמיד true
  2. x.Equals(y) תמיד שווה ל y.Equals(x)
  3. אם x.Equals(y) וy.Equals(z) אז גםx.Equals(z)
  4. x.Equals(null) יחזיר תמיד false
  5. הפונקציה Equals לעולם לא תזרוק Exceptionים
  6. קריאה לEquals צריכה להיות קונסיסטנטית, כלומר אם נקרא לx.Equals(y) מספר פעמים, צריכה לחזור אותה תוצאה, כל עוד לא שינינו את x או את y.

שלושת התנאים הראשונים הם אקסיומות של יחס שקילות.

בנוסף, יש לדאוג לדרוס את GetHashCode.

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

  1. אם דורסים את Equals, יש לדרוס גם את GetHashCode ולהפך
  2. צריך להתקיים שאם x.Equals(y), אז יש ל xול yאותו HashCode, (כלומר (x.GetHashCode() == y.GetHashCode()
  3. אסור לפונקציה לזרוק Exceptionים
  4. GetHashCode צריכה להיות קונסיסטנטית: כל עוד לא שינינו את x, צריך לחזור אותו ערך בקריאה x.GetHashCode().

נסיים בהמלצה:

אם אתם דורסים את Equals, תממשו גם IEquatable של הType שלכם.

זה דורש אפס עבודה, אם אתם מממשים Equals בצורה הבאה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
if (obj is MyEquatable)
{
return this.Equals((MyEquatable) obj);
}
// Other cases...
}
public bool Equals(MyEquatable other)
{
// ...
}

למה זה טוב? זה מאפשר לקרוא לEquals על הטיפוס שלכם בלי boxing מה שעשוי גם לשפר ביצועים.

הרבה פונקציות של הFramework משתמשות בזה, ובודקות ראשית אם אתם מממשים את זה, לפני שהן קוראות לEquals הוירטואלי של object.

המשך יום שווה

שתף

76. The difference between Equals and == operator

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

קיימים שתי דרכים פופולאריות לעשות זאת

הדרך היותר פופולארית: להשתמש באופרטור ==:

1
2
3
4
5
6
7
string myFirstString = "Hello";
string mySecondString = "Hello!";
if (myFirstString == mySecondString)
{
// ...
}

הדרך השנייה היא באמצעות המתודה הוירטואלית Equals:

1
2
3
4
if (myFirstString.Equals(mySecondString))
{
// ...
}

האם אי פעם תהיתם מה ההבדל בין המתודות?

ראשית הoperator == היא מתודה סטטית ואילו המתודה Equals היא מתודה וירטואלית של object.

במה מתבטא ההבדל?

שימו לב לקוד הבא:

1
2
3
4
5
6
7
int myFirstNumber = 3;
int mySecondNumber = 3;
if (myFirstNumber == mySecondNumber) // true
{
//
}

ולקוד הבא

1
2
3
4
5
6
7
object myFirstNumber = 3;
object mySecondNumber = 3;
if (myFirstNumber == mySecondNumber) // false
{
//
}

לעומת זאת אם היינו כותב Equals במקום, זה היה עובד. למה?

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

לעומת זאת Equals היא פונקציה וירטואלית ומפוענחת בזמן ריצה.

אז למה בדוגמה השנייה זה לא נכנס לif?

האופרטור == מפוענח בזמן קימפול בתור הפונקציה הסטטית הזו:

1
public static bool operator == (object a, object b)

ה== של object משווה Referenceים. מאחר ומתבצע פה boxing, יש פה שני referenceים שונים, ולא מדובר באותו אובייקט.


באופן כללי קיימות שתי הפונקציות.

המטרה של Equals זה לבדוק ששני אובייקטים שווים באיזשהו מובן (למשל, נוכל ליצור מחלקת string משלנו, שבה "Hello" ו"hello" יהיו שווים בEquals)

לעומת זאת, המטרה של == היא לבדוק ששני אובייקטים זהים תמיד (כלומר "אותו הדבר")

לדוגמה:

1
2
3
4
5
6
7
8
9
10
11
12
13
StringBuilder firstBuilder =
new StringBuilder("Hello");
StringBuilder secondBuilder =
new StringBuilder("Hello");
if (firstBuilder.Equals(secondBuilder)) // true
{
}
if (firstBuilder == secondBuilder) // false
{
}

שימו לב שEquals מחזיר true לעומת == שמחזיר false. למה זה הגיוני?

כרגע מבחינת תוכן שני הbuilderים האלה הם שווים.

לעומת זאת, הם לא בהכרח יהיו תמיד זהים, כלומר נוכל לשנות את הbuilder הראשון ואז לא יהיו שווים מבחינת הEquals. לכן הגיוני שהם לא זהים.

שימו לב שזה עניין של סמנטיקה. יכלו גם לדרוס את == ולדאוג שהוא יתנהג כמו Equals.


לבסוף, נסביר למה בעצם קיימות שתי הפונקציות. יש לזה 3 סיבות עיקריות:

  1. הקריאה a.Equals(b) זורקת Exception אם האיבר הראשון הוא null, לעומת הקריאה a == b
  2. הoperator הסטטי היא פונקציה שמפוענחת בזמן קימפול, ולכן עלותה נמוכה יותר
  3. לפעמים היינו רוצים שEquals ו== לא יחזירו אותו ערך

שבוע שווה!

שתף

75. string Contains extension method

ראינו בעבר (טיפ מספר 5) כי יש overload לstring של Equals המאפשר לנו להשוות מחרוזות בהתעלמות מcase sensitive.

לפעמים אנחנו רוצים לבדוק דברים אחרים, כמו Contains.

למרבה הצער, לו אין overload דומה.

במקום לכתוב ככה:

1
2
3
4
if (first.ToUpper().Contains(second.ToUpper()))
{
// ...
}

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

במקום זאת, נוכל ליצור Extension Method כזה ולהשתמש בו:

1
2
3
4
5
6
7
8
9
10
11
12
public static bool Contains(this string givenString,
string value,
StringComparison comparisonType)
{
if (givenString == null)
{
throw new ArgumentNullException("givenString",
"Received null");
}
return (givenString.IndexOf(value, comparisonType) >= 0);
}

בדרך זו נוכל לבדוק את התנאי בצורה הבאה:

1
2
3
4
5
if (first.Contains(second,
StringComparison.InvariantCultureIgnoreCase))
{
// ...
}

יותר יעיל ויותר יפה.

סופ"ש מורחב טוב

שתף

74. ICollection AddRange extension method

מדי פעם אנחנו משתמשים בICollection ולא בטיפוס ספציפי (כמו List)

שמתי לב שאין אף ממשק עם פונקציה של AddRange. אפשר לפתור את הבעיה ככה:

ניצור פשוט Extension Method כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void AddRange<T>(this ICollection<T> collection,
IEnumerable<T> range)
{
if (collection == null)
{
throw new ArgumentNullException
("collection", "Received null");
}
foreach (T current in range)
{
collection.Add(current);
}
}

הבעיה היא שמבט חטוף בReflector מראה שבList<T> יש מימוש יותר מסובך מהמימוש הנאיבי, כנראה טיפה יותר טוב באיזושהי צורה. לכן נוסיף טיפול מיוחד במקרה זה:

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 static void AddRange<T>(this ICollection<T> collection,
IEnumerable<T> range)
{
if (collection == null)
{
throw new ArgumentNullException
("collection",
"Received null");
}
List<T> list = collection as List<T>;
if (list != null)
{
list.AddRange(range);
}
else
{
foreach (T current in range)
{
collection.Add(current);
}
}
}

כעת נוכל לכתוב קוד כזה בכיף:

1
2
3
4
5
6
7
8
9
10
ICollection<string> favoriteSongs =
new Collection<string>();
favoriteSongs.AddRange(new[]
{
"Shine on you crazy diamond",
"Wish you were here",
"Another brick in the wall",
"Hey you"
});

קצת מזכיר את הכתיבה של Collection Initializer (ראו גם טיפ מספר 18), אלא שאפשר להשתמש בזה גם לא בConstructor, ולמעשה אפשר לעשות שילובים יפים כאלה:

1
2
3
4
5
6
7
favoriteSongs.AddRange(new List<string>
{
"Shine on you crazy diamond",
"Wish you were here",
"Another brick in the wall",
"Hey you"
});

יתרה מזו, בהינתן שAddRange של List<T> יותר טוב מזה שאנחנו כתבנו, אז הכתיבה הבאה עדיפה על הכתיבה עם Collection Initializer:

1
2
3
4
5
6
7
8
9
10
ICollection<string> favoriteSongs =
new List<string>();
favoriteSongs.AddRange(new []
{
"Shine on you crazy diamond",
"Wish you were here",
"Another brick in the wall",
"Hey you"
});

עדיפה מזו:

1
2
3
4
5
6
7
8
ICollection<string> favoriteSongs =
new List<string>
{
"Shine on you crazy diamond",
"Wish you were here",
"Another brick in the wall",
"Hey you"
};

מהסיבה ששימוש בCollection Initializer שקול לשימוש בפונקציה שאנחנו כתבנו.

יום מורחב טוב

שתף

73. Dictionary Add extension method

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

1
Dictionary<string, ICollection<int>>

זהו Dictionary שהValues שלו הם Collection, למשל PKים של אנשים שהשם שלהם הוא מסוים וכו’.

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

1
2
3
4
5
6
7
8
9
10
11
12
IDictionary<string, ICollection<int>> namesToPks =
new Dictionary<string, ICollection<int>>();
ICollection<int> requestedNamesPks;
if (!namesToPks.TryGetValue(givenName, out requestedNamesPks))
{
requestedNamesPks = new List<int>();
namesToPks.Add(givenName, requestedNamesPks);
}
requestedNamesPks.Add(givenPk);

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

ניצור Extension Method כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void Add<TKey, TValue, TValueCollection>
(this IDictionary<TKey, TValueCollection> dictionary,
TKey key,
TValue value)
where TValueCollection : ICollection<TValue>, new()
{
TValueCollection requestedValue;
bool wasValueFound =
dictionary.TryGetValue(key, out requestedValue);
if (!wasValueFound)
{
requestedValue = new TValueCollection();
dictionary.Add(key, requestedValue);
}
requestedValue.Add(value);
}

עכשיו נוכל לכתוב קוד כזה בכיף ובניחותא:

1
2
3
4
IDictionary<string, List<int>> namesToPks =
new Dictionary<string, List<int>>();
namesToPks.Add(givenName, givenPk);

שימו לב שהקומפיילר מגלה Implicitly מהם הפרמטרים הגנריים!

יותר יפה, לא?

הבעיה היחידה כאן היא שאנחנו חייבים של TValueCollection יהיה Constructor ולכן לא יכול להיות ICollection<TValue>.

במקום זאת נוכל לעשות Extension Method טיפה יותר גנרי:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void Add<TKey, TValue, TValueCollection, TCollection>
(this IDictionary<TKey, TValueCollection> dictionary,
TKey key,
TValue value)
where TValueCollection : ICollection<TValue>
where TCollection : TValueCollection, new()
{
TValueCollection requestedValue;
bool wasValueFound =
dictionary.TryGetValue(key, out requestedValue);
if (!wasValueFound)
{
requestedValue = new TCollection();
dictionary.Add(key, requestedValue);
}
requestedValue.Add(value);
}

וליצור שני overloadים:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void Add<TKey, TValue, TValueCollection>
(this IDictionary<TKey, TValueCollection> dictionary,
TKey key,
TValue value)
where TValueCollection : ICollection<TValue>, new()
{
dictionary.Add
<TKey, TValue, TValueCollection, TValueCollection>
(key, value);
}
public static void Add<TKey, TValue>
(this IDictionary<TKey, ICollection<TValue>> dictionary,
TKey key,
TValue value)
{
dictionary.Add
<TKey, TValue, ICollection<TValue>, List<TValue>>
(key, value);
}

וכעת נוכל לקרוא בשתי הדרכים:

1
2
3
4
IDictionary<string, ICollection<int>> namesToPks =
new Dictionary<string, ICollection<int>>();
namesToPks.Add(givenName, givenPk);

וגם בדרך הזו:

1
2
3
4
IDictionary<string, List<int>> namesToPks =
new Dictionary<string, List<int>>();
namesToPks.Add(givenName, givenPk);

שיהיה אחלה יום מורחב

שתף

72. Enum HasFlag extension

ראינו בעבר (טיפ מספר 22) כיצד אנחנו יכולים לבדוק שיש לEnum איזשהו Flag.

הדרך הייתה כזו:

1
2
3
4
if ((direction & Direction.South) == Direction.South)
{
// ...
}

הבעיה היא שלא כל כך כיף לכתוב את זה.

עכשיו, אחרי שלמדנו על Extension Methods נוכל לעשות משהו עם Syntax טיפה יותר יפה:

ניצור Extension Method כזה:

1
2
3
4
5
6
public static bool HasFlag(this Enum givenEnum, Enum flagEnum)
{
ulong givenValue = Convert.ToUInt64(givenEnum);
ulong flagValue = Convert.ToUInt64(flagEnum);
return ((givenValue & flagValue) == flagValue);
}

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

1
2
3
4
if (direction.HasFlag(Direction.South))
{
// ...
}

שימו לב שהסינטקס יותר יפה, אבל יש פה מספר בעיות:

  1. יש Boxing לEnum בקריאה לפונקציה
  2. אנחנו ממירים את הערכים לulong
  3. אין לנו התחייבות ששני הEnumים מאותו סוג

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

אגב, בFramework 4.0 הוסיפו מתודה לEnum שנקראת HasFlag אשר בודקת אם יש לEnum דגל. המימוש הוא מאוד דומה למה שכתבנו פה.

בכל מקרה זה בעיקר נחמד.

הערה:

ניתן לעשות כך :

1
2
3
4
5
6
7
public static bool HasFlag<T>(this T value, T flag)
where T : IConvertible
{
long underValue = value.ToInt64(null);
long underFlag = flag.ToInt64(null);
return ((underFlag & underValue) == underFlag);
}

כך בעצם נחסוך כמה דברים :

  1. גנריות – נוכל להשתמש בפונקציה הנ"ל על כל דבר שמממש IConvertible – אפילו Stringים מממשים את זה, וכל הPrimitives.
  2. אין Boxing
  3. יש התחיבות ששני הEnumים הם מאותו הסוג.
שתף