291. About Contains extension method

יצא לנו להכיר את הExtension Methods שיש בLINQ.

חלק מהExtension Methods יודעות לבצע אופטימיזציות במידה והIEnumerable שלנו הוא טיפוס עם יותר פונקציונאליות.

ראינו, למשל, בטיפ מספר 15, שCount() ממומש ע”י גישה פשוטה לProperty בשם Count, במידה והIEnumerable הוא ICollection.

אולי זה מפתיע, אבל ישנו מקרה בו אופטימיזציות אלה גורמות להתנהגות מעט מוזרה.

המקרה הוא הExtension Method של Contains. זה נראה כמו Extension Method סטנדרטי:

1
2
3
4
5
6
7
8
9
10
11
public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value)
{
ICollection<TSource> is2 = source as ICollection<TSource>;
if (is2 != null)
{
return is2.Contains(value);
}
return source.Contains<TSource>(value, null);
}

יש כאן אופטימיזציה: אם הטיפוס הוא ICollection<TSource>, פשוט מבצעים הסבה וקוראים לContains. אלא שהדבר הזה יכול לגרור התנהגות לא ברורה:

אם נניח נריץ את הקוד הבא:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
IEnumerable<string> names =
new HashSet<string>(StringComparer.InvariantCultureIgnoreCase)
{
"Jason",
"jameson",
"jeFferson",
"JacKson",
"Robert Lewis Stevenson"
};
if (names.Contains("jason"))
{
// true
}

לכאורה תקין. האוסף אמנם לא מכיר את המחרוזת הLower case, אבל הוא מכיל את המחרוזת "Jason", ולכן אולי אפשר להניח שזה הגיוני.

קונספטואלית זה דווקא לא הגיוני. הרי Contains הוא Extension Method של IEnumerable<T> ולכן היינו מצפים שהוא בודק האם האיבר קיים כאיבר המתקבל בריצה על הEnumerable באמצעות הComparerהדיפולטי. אם היינו רוצים לציין לו EqualityComparer שהוא ישתמש בו, היינו משתמשים בOverload השני המקבל EqualityComparer.

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

נניח שהEnumerable באמת מכיל את המחרוזת הLower case. אז היינו מצפים שגם הEnumerable הבא יכיל אותה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
IEnumerable<string> names =
new HashSet<string>(StringComparer.InvariantCultureIgnoreCase)
{
"Jason",
"jameson",
"jeFferson",
"JacKson",
"Robert Lewis Stevenson"
};
IEnumerable<string> lazyNames = names.Select(x => x);
if (lazyNames.Contains("jason"))
{
// false
}

אלא שלא. כי הוא באמת משווה את האיברים כאיברים שמתקבלים ע"י ריצה על Enumerable.

את הבעיה הזו מצא Jon Skeet כשהוא ניסה לממש בעצמו את כל כל הפונקציות מLINQ, וראה שהUnit Testים שלו עוברים במקום שאלה של System.Linq נכשלים.

סופ"ש שמכיל דברים כאיברים המתקבלים בריצה על Enumerable טוב.

שתף