312. About Enumerable Repeat

הכרנו בעבר (ראו גם טיפ מספר 153) את הפונקציה Enumerable.Repeat המאפשרת לנו ליצור אוסף המכיל איבר מסוים מספר נתון של פעמים.

הפונקציה כאמור מקבלת איבר ומספר שלם, ויוצרת אוסף המכיל את האיבר לפי המספר הנתון של פעמים.

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

נניח שאנחנו רוצים ליצור מערך חדש ולאתחל כל איבר בו בערך חדש, למשל. הדרך הקלאסית לעשות משהו כזה היא:

1
2
3
4
5
6
Person[] people = new Person[100];
for (int i = 0; i < people.Length; i++)
{
people[i] = new Person();
}

בעזרת הOverload שיש לנו, אנחנו יכולים לעשות משהו כזה:

1
Person[] people = Enumerable.Repeat(new Person(), 100).ToArray();

אלא שזה לא ייתן לנו אותה תוצאה, מאחר ומה שיקרה בשיטה השנייה שרשמנו זה יווצר מערך שכל התאים מחזיקים Reference לאותו הInstance של Person.

אם היה לנו את הOverload שדיברתי עליו, היינו יכולים לעשות משהו כזה:

1
Person[] people = Enumerable.Repeat(() => new Person(), 100).ToArray();

למעשה זה לא מסובך לממש Overload כזה, וזה נראה בערך ככה:

1
2
3
4
public static IEnumerable<T> Repeat<T>(Func<T> creator, int count)
{
return Enumerable.Range(0, count).Select(x => creator());
}

(ראו גם טיפ מספר 152, 92)

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

ברגע שאנחנו לא קוראים לToArray, מתחילה הבעייתיות.

אפשר באופן תמים לחלוטין לכתוב קוד כזה:

1
2
3
4
5
6
7
8
IEnumerable<Person> people = Repeat(() => new Person(), 100);
Random random = new Random();
foreach (Person person in people)
{
person.Id = random.Next();
}

הקוד נראה תקין לחלוטין, אנחנו מאתחלים אוסף של Person, ואז דואגים לאתחל לכל אחד מהם את הId לאיזשהו מספר אקראי (במקום 0).

אלא שאם תריצו את הקוד הבא, תגלו את ההפתעה הבאה:

1
2
3
4
foreach (Person person in people)
{
Console.WriteLine(person.Id); // 0
}

כן, כן, כל הIdים מחזירים 0.

האם הRandom לא עבד טוב? האמת שהוא עבד מצוין. אז מה הבעיה?

כזכור (טיפים על LINQ – למשל מספרים 91-94), כאשר אנחנו מריצים LINQ, כל האוספים שחוזרים לנו הם Lazy.

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

אם נזכר, זה בין השאר מאחר וLINQ מאוד מבוססת תכנות פונקציונאלי, וחלק מהעקרונות של תכנות פונקציונאלי הם אובייקטים שהם Immutable (טיפ מספר 271) וStateless.

לכן כל פעם שנרוץ על people, יחושב מחדש האוסף, ולמעשה יאותחלו מחדש כל איבריו.


מה המסקנה? יש דברים שלא מתערבבים. לא סתם לא הכניסו לLINQ כל מיני Extension Methods כמו ForEach, הסיבה היא שזה נוגד את העקרונות שLINQ נבנה עליהם.

אם אנחנו רוצים ליצור מתודה כזאת בשביל לקצר את תהליך אתחול מערך, אולי עדיף שהמתודה הזאת תחזיר מערך בעצמה ולא IEnumerable כללי.

ואולי עדיף לא להכניס מתודה כזאת, אלא לאתחל מערך בשיטה הקלאסית.

המשך יום שלא חוזר על עצמו יותר מדי טוב.

שתף