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.

המשך יום שווה

שתף