311. Multiple enumeration on a given IEnumerable

לא אחת יוצא לנו לכתוב פונקציות שמקבלות IEnumerable כפרמטר.

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

את הבדיקות האלה אנחנו מבצעים בעזרת הExtension Methods של LINQ. מה שהן עושות מאחורי הקלעים זה קוראות לGetEnumerator() ומתחילות לרוץ על האוסף. (ראו טיפים מספר 51, 15, 91-105 ,291 ועוד)

הדבר הזה יכול להיות מסוכן, במידה ואנחנו קוראים ליותר מExtension Method אחד כזה, מאחר וזה גורם ליותר מריצה אחת על האוסף.

למה זה יכול להיות מסוכן? ראו למשל את הקוד הבא:

1
2
3
4
5
6
7
8
9
10
11
private static int mCallCount = 0;
public IEnumerable<int> GetRange()
{
mCallCount++;
for (int i = 0; i < mCallCount; i++)
{
yield return (i + mCallCount);
}
}

הקוד הזה מחזיר לנו IEnumerable, אבל כל פעם שנרוץ עליו הוא יכיל איברים שונים. בהתחלה הוא יכיל את המספר 1.

אחר כך את המספרים 2,3. אחר כך את המספרים 3,4,5.

(ראו טיפ מספר 54-55 אם אתם לא מכיר את yield return)

למשל אם נריץ את הקוד הבא נקבל:

1
2
3
4
5
6
IEnumerable<int> range = GetRange();
Console.WriteLine(range.First()); // 1
Console.WriteLine(range.First()); // 2
Console.WriteLine(range.First()); // 3
Console.WriteLine(range.First()); // 4

הReSharper יודע לזהות את הסיפור הזה ומציג אזהרה לקוד החל מReSharper 6.

אז מה בעצם אפשר לעשות? מצד אחד אפשר לומר שזו בעיה של המשתמש – אם הוא שלח לנו לפונקציה איזשהו IEnumerable שהוא לא עקבי, זו בעיה שלו, והוא עשוי לקבל פלט לא צפוי מהפונקציה.

אופציה אחרת היא למנוע ריצה על האוסף יותר מפעם אחת, וזה ע"י קריאה לExtension Method ששמו ToList() אשר יוצר רשימה שמכילה את כל איברי הIEnumerable ע"י ריצה עליו, ואז לבצע את כל הפעולות מול הרשימה הזאת. אלא שגם דרך זה אינה חסינה ממספר בעיות: מה אם הIEnumerable הוא אינסופי? או מה אם במהלך הריצה, מוחזר תמיד אותו Reference של אובייקט, אבל האובייקט משתנה (ראו לדוגמה את הטיפ עלScan, המאפשר לעשות דבר כזה, טיפ מספר 305)

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

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

המשך יום בלי יותר מריצה אחת.

הערה: דרך נוספת, אם בכל זאת רוצים לעשות binding למשתנה אשר נמצא מחוץ ל- scope, היא להשתמש בעותק מקומי שלו:

(למרות שבמקרה ספציפי זה זה לא נכון).

1
2
3
4
5
6
7
8
9
10
11
private static int mCallCount = 0;
public IEnumerable <int> GetRange()
{
var callCount = mCallCount + 1;
for (int i = 0; i < callCount; i++)
{
yield return (i + callCount);
}
}
שתף