כמו במקרה של Equals, לפעמים אנחנו מעוניינים לבצע מיון לא לפי הפונקציה CompareTo של הממשק IComparable, אלא דווקא בצורה אחרת.
לצורך פתרון זה, ניתן להשתמש בOverload של OrderBy שמקבל IComparer לפיו נבצע את ההשוואה.
לדוגמה (מהחיים האמיתיים), נניח שיש לנו רשימה של כתובות של תיקיות ואנחנו מעוניינים למיין אותם לפי רמת הקינון שלהם (כלומר שיופיעו קודם תיקיות אב, ורק אחר כך תיקיות בנות)
נוכל לעשות זאת בצורה הבאה:
נממש IComparer:
1
2
3
4
5
6
7
public class FolderLocationComparer : IComparer<string>
{
publicintCompare(string x, string y)
{
// Use Path class to implement this.
}
}
כעת נבצע מיון באמצעותו:
1
2
IEnumerable<string> orderedByHierarchy =
folders.OrderBy(x => x, new FolderLocationComparer());
כמו במקרה של Equals, לפעמים אנחנו מעוניינים להשוות איברים, באמצעות השוואה אחרת.
למשל, אנחנו עשויים לרצות להשוות מחרוזות דווקא לפי האורך שלהן, ולא לפי המקום שלהם במילון.
באופן אנלוגי לIEqualityComparer (טיפ מספר 111) המאפשר לנו להשוות איברים בצורה מלאכותית, ולאו דווקא באמצעות פונקצית הEquals של האובייקט, קיים פתרון מקביל גם עבור IComparable.
הפתרון הוא ממשק ששמו IComparer המאפשר לנו להגדיר כיצד להשוות איברים מבחוץ. יש לו פונקציה אחת:
1
publicintCompare(T x, T y)
אותה אנחנו צריכים לממש, ולהחליט כיצד להשוות איברים.
קיימת גם גרסה לא גנרית של ממשק זה המשווה objectים. במידה ואנחנו מעוניינים לממש את שני הממשקים, נוכל לרשת מהטיפוס Comparer<T>. כך נצטרך רק לממש את הפונקציה הגנרית, ונקבל מימוש Out of the boxשל המימוש של objectים.
למשל, בדוגמה של השוואת מחרוזות לפי האורך שלהן:
1
2
3
4
5
6
7
public class StringLengthComparer : Comparer<string>
{
publicoverrideintCompare(string x, string y)
{
return (x.Length - y.Length);
}
}
השוואה מתבצעת באמצעות:
1
2
3
4
5
6
StringLengthComparer comparer = new StringLengthComparer();
if (comparer.Compare("Hellllo", "world") > 0)
{
// True
}
בנוסף, אם אנחנו מעוניינים מאיזושהי סיבה לקבל Comparer שמשתמש בפונקציה CompareTo, במידה והטיפוס שלנו מממש IComparable<T>, נוכל להשתמש בComparer הסטטי הבא:
1
Comparer<T>.Default
כמובן, לרוב הפונקציות שמשתמשות בממשק IComparable<T> בשביל להשוות איברים, קיים overload שמאפשר לציין באמצעות איזה Comparer להשוות.
לפעמים במקום למצוא רק לסנן את האיברים שלנו, כך שכל איבר יופיע רק פעם אחת, אנחנו מעוניינים לעשות משהו אחר – לקבץ את האיברים לפי איזושהי תכונה שלהם
למשל, נניח שיש לנו רשימה של אנשים ואנחנו מעוניינים לקבץ אותם לפי הגיל שלהם, כך שלכל גיל נוכל לראות את כל האנשים בגיל זה.
במקום להשתמש בDistinct ולשלב את זה עם שאילתא אחרת, אפשר להשתמש בExtension Method של LINQ ששמו GroupBy:
1
2
IEnumerable<IGrouping<int, Person>> peopleByAge =
people.GroupBy(person => person.Age);
הפונקציה מקבלת delegate שמייצג לפי מה אנחנו מעוניינים לקבץ.
אתם בוודאי שואלים את עצמכם מהו IGrouping<int, Person>? זהוIEnumerable<Person> שיש לו Key שמייצג את מה שהקיבוץ נעשה לפיו.
נוכל לעשות משהו כזה:
1
2
3
4
5
6
7
8
9
10
11
12
foreach (IGrouping<int, Person> currentGroup in peopleByAge)
{
Console.WriteLine("The following are {0} years old:",
currentGroup.Key);
foreach (Person person in currentGroup)
{
Console.WriteLine("{0} {1}",
person.FirstName,
person.LastName);
}
}
יש עוד כמה overloadים שמאפשרים לנו להחליט מה אנחנו מעוניינים לקבץ, וכמובן, איך אפשר בלי, הIEqualityComparer שאיתו אנחנו מעוניינים להשוות איברים.
משהו מגניב שאפשר לעשות: למשל, אנחנו מעוניינים לקטלג אנשים לפי טווח גילאים: 0-12 ילדים, 13-18 נוער, 19 ומעלה מבוגרים. נוכל ליצור IEqualityComparer<int> שמשווה את הגילאים לפי הקריטריונים שכתבתי פה, ולהעביר אותו לקיבוץ:
1
2
3
4
IEqualityComparer<int> ageComparer = new AgeComparer();
איך זה ממומש מאחורי הקלעים? מסתבר שכמה אנשים מוטרדים משאלה זו.
ובכן, יש HashSet בצד, שבכל איטרציה מוסיפים אליו את האיבר במידה והוא לא נמצא בו ומחזירים אותו, ואחרת מדלגים עליו.
שימו לב שבזכות העובדה שזהו HashSet, כל פעם שאנחנו מוסיפים איבר, אנחנו לא צריכים לעבור על כל האיברים כדי לענות על השאלה האם האיבר כבר הופיע, אלא בגלל שמדובר בHashSet, זה מתבצע כמעט ב$ O\left(1\right) $. (ראו גם טיפ מספר 19)
מה הבעיה פה? כמו בטיפ מספר 5, בכל פעם שאנחנו ניגשים לDictionary, אנחנו בעצם יוצרים אובייקט חדש (ע"י קריאה לToUpper).
כלומר כל גישה יוצרת אובייקט חדש, וזה אובייקט מבוזבז – מבזבז לנו זמן (ביצירה) וזכרון.
במקום, נוכל להיזכר בקיומם של
בFramework. מסתבר שישConstructor של הטיפוס Dictionary<TKey, TValue> שמקבל IEqualityComparer<TKey> לפיו הוא משווה ערכים.
1
2
3
4
5
6
בזכות הטיפ היומי של אתמול נוכל לעשות משהו כזה:
```csharp
IDictionary<string, object> nameToValue =
new Dictionary<string, object>(new StringIgnoreCaseSensitiveComparer());
וכעת נוכל לגשת לDictionary בשיטה הרגילה
1
2
nameToValue[givenName] = givenValue;
requestedValue = nameToValue[givenName];
בלי ליצור אובייקט חדש בכל גישה לDictionary.
הערה: המימוש של אתמול של StringIgnoreCaseSensitiveComparer דווקא כן יוצר אובייקט חדש בכל גישה לDictionary, כי אנחנו קוראים בGetHashCode לToUpper. נראה בהמשך דרך אחרת להשתמש בEqualityComparer שמתעלם מCase sensitive, ולא יוצר אובייקט חדש בכל גישה.