265. IStructuralEquatable interface

הכרנו בעבר את הממשק IEqualityComparerהמאפשר לנו לציין כיצד להשוות שני איברים מטיפוס כלשהו. (ראו גם טיפים מספר 76-80, 111-116)

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

הרי שוויון מערכים הוא לפי Referenceים:

1
2
3
4
5
6
int[] theNumbers = new int[] {4, 8, 15, 16, 23, 42};
int[] evil = new int[] {4, 8, 15, 16, 23, 42};
if (theNumbers.Equals(evil))
{
// False
}

אלא שקצת מסורבל לעשות זאת.

בFramework 4.0 החליטו להוסיף ממשק בשם IStructuralEquatable המפשט את הפתרון של בעיות אלה. הממשק הוא ממשק המציין שאפשר להשוות את האובייקט שלנו עם Instance אחר, לפי המבנה הפנימי.

למה הכוונה? אם נזכר למשל בטיפ מספר 77, מתקיים ששוויון Equals של Value Types, מתקיים אם הStructים שווים רכיב רכיב, כלומר שכל השדות של הInstance הראשון שווים לכל השדות של הInstance השני.

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

איך זה נראה?

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

1
2
3
4
5
public interface IStructuralEquatable
{
bool Equals(object other, IEqualityComparer comparer);
int GetHashCode(IEqualityComparer comparer);
}

שימו לב שאין פה פרמטרים גנריים בניגוד לגרסה הגנרית שלIEqualityComparer אותה זכינו לפגוש, נסביר למה בהמשך.

מה שאנחנו אמורים לעשות זה להשוות את הInstance של האובייקט שלנו עם Instance אחר לפי הComparer שקיבלנו.

למשל, במערכים היינו מממשים משהו כזה:

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
public bool Equals(object other, IEqualityComparer comparer)
{
Array otherArray = other asArray;
if (otherArray == null)
{
return false;
}
else if (this.Length != otherArray.Length)
{
return false;
}
else
{
for (int i = 0; i <this.Length; i++)
{
if (!comparer.Equals(this[i], otherArray[i]))
{
return false;
}
}
return true;
}
}

לValue Types היינו מצפים למימוש כזה:

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
public struct MyStruct : IStructuralEquatable
{
private int mNumber;
private string mName;
public bool Equals(object other, IEqualityComparer comparer)
{
if (!(other is MyStruct))
{
return false;
}
MyStruct otherStruct = (MyStruct) other;
if (!comparer.Equals(mNumber, otherStruct.mNumber))
{
return false;
}
if (!comparer.Equals(mName, otherStruct.mName))
{
return false;
}
return true;
}
}

אם תהיתם עד עכשיו למה הIEqualityComparer הוא לא גנרי, עכשיו קיבלנו סיבה: הסיבה היא שאנחנו אמורים להשתמש בו כדי להשוות את כל האיברים הפנימיים!

מה בנוגע לGetHashCode? זה יותר מסובך, אבל מימוש מקובל הוא לבצע XOR על הGetHashCodeים של כל האיברים הפנימיים עם הComparer, ולהחזיר את התוצאה.

למרבה השמחה, יש מספר טיפוסים בFramework שמממשים ממשק זה. ביניהם: מערכים וTupleים (ראו גם טיפ מספר 126). לValue Types דווקא אין מימוש דיפולטי לממשק זה.

המימוש אצל מערכים וTupleים הוא Explicit (ראו גם טיפ מספר 82):

1
2
3
4
5
6
7
8
9
int[] theNumbers = new int[] { 4, 8, 15, 16, 23, 42 };
int[] evil = new int[] { 4, 8, 15, 16, 23, 42 };
IStructuralEquatable theNumbersStructuralEquatable = theNumbers;
if (theNumbersStructuralEquatable.Equals( evil,EqualityComparer<int>.Default))
{
// True
}

ואפשר לדחוף גם EqualityComparers יותר מעניינים:

1
2
3
4
5
6
7
8
9
string[] tokens = new string[] {"The", "quick", "brown", "fox","jumps", "over", "the", "lazy","dog"};
string[] otherTokens = new string[] { "The", "Quick", "Brown", "fox","jUmps", "ovEr", "the", "lazy","Dog" };
IStructuralEquatable tokensStructuralEquatable = tokens;
if(tokensStructuralEquatable.Equals(otherTokens, StringComparer.CurrentCultureIgnoreCase))
{
// True
}

שבוע מובנה טוב!

שתף