מה זה foreach וכיצד מממשים אותו.
לאלו שנכנסים לעולם הפיתוח ולומדים #C, כשמגיעים ללולאות לומדים שיש כמה סוגים, כשהנפוצים הם, for ו - foreach,
ההסבר ללולאת for הוא די פשוט, לדוגמא:
int[] arr = { 1, 2, 3, 4, 5 };
for (var i = 0; i < arr.Length; i++)
{
Console.WriteLine(arr[i];
}
מבצעים את שורת הקוד שבתוף גוף הלולאה כגודל המערך ובכל פעם מדפיסים מספר אחד מתוך המערך.
אותה תוצאה בתחביר foreach
foreach (int item in arr)
{
Console.WriteLine(item);
}
איכשהו המשתנה item יהיה כל פעם הערך הבא במערך, ובפוסט הזה אני אסביר כיצד ממומש foreach.
ניקח לדוגמא את המחלקה הבאה
class Person
{
public int Age { get; set; }
public string Name { get; set; }
}
class PersonManager
{
private List<Person> _persons;
public PersonManager()
{
_persons = new List<Person>();
}
}
כמובן שיש הרבה סיבות למה להשתמש ב - PersonManager ולא להשתמש ישירות באוסף של Persons, זה יכול להיות כדי לעשות בדיקות לפני שמוסיפים Person למערך או כל סיבה אחרת,
כעת נניח שיש לנו מופע של PersonManager, ונרצה איכשהו לרוץ על כל ה - Persons שהוא מחזיק, לא נוכל כמובן לרוץ בלולואת for, מכיוון שהמשתנה persons_ מוגדר כ - private ומסיבות מובנות ולא נרצה כמובן לחשוף אותו,
פיתרון סביר יהיה לגרום שנוכל לכתוב קוד כזה:
PersonManager manager = new PersonManager();
foreach (Person item in manager)
{
}
כמובן שממבט ראשון זה נראה מוזר, מכיוון ש - manager אינו מערך, אז כיצד נגרום לו לאפשר את הקוד הזה, גם הקומפיילר לא יאהב את זה וייתן את הודעת השגיאה
foreach statement cannot operate on variables of type 'ConsoleApplication7.PersonManager' because 'ConsoleApplication7.PersonManager' does not contain a public definition for 'GetEnumerator'
כדי לפתור זאת נצטרך לממש את IEnumerable
class PersonManager : IEnumerable
{
private List<Person> _persons;
public PersonManager()
{
_persons = new List<Person>();
}
IEnumerator GetEnumerator()
{
}
}
אנחנו רואים שאנחנו צריכים לממש מתודה בשם GetEnumerator שמחזירה מישהו שמממש את IEnumerator, (בהמשך אני אסביר מדוע צריך שני interfaces שונים)
כעת נייצר מחלקה חדשה שתמממש את ה - interface הנדרש, ובשביל הנוחות נגדיר את המחלקה החדשה בתוך המחלקה PersonManager.
ה - interface מוגדר כך:
public interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
}
המאפיין Current אמור להחזיר את האובייקט הנוכחי בריצה של הלולאה.
הפונקצייה MoveNext אמורה לגרום לכך ש - Current יצביע למיקום המתאים, ובנוסף להחזיר ערך בוליאני המגידר האם יש מקום לסיבוב נוסף של הלולאה.
לפונקציה Reset אין משמעות.
וכך זה יראה:
class PersonManagerEnumerator : IEnumerator
{
private PersonManager _parent;
private int _index = -1;
public PersonManagerEnumerator(PersonManager manager)
{
_parent = manager;
}
public object Current
{
get
{
return _parent._persons[_index];
}
}
public bool MoveNext()
{
_index++;
return _index < _parent._persons.Count;
}
public void Reset()
{
throw new NotImplementedException();
}
}
ב - ctor של המחלקה אחנו מקבלים מופע של PersonManager (ולכן חשוב שהמחלקה תהיה מוגדרת בתוך המחלקה PersonManager כך שתהיה גישה ל - private members), במתודה MoveNext אנחנו כל פעם מקדמים את ה - index שמוחזר על ידי ה - Current. (וזה דרך אגב הסיבה שאי אפשר לשנות ערכים של המערך ב - foreach, מכיוון שה - Current מוגדר רק עם get)
כעת נחזור למתודה הקודמת, שתראה כך:
public IEnumerator GetEnumerator()
{
return new PersonManagerEnumerator(this);
}
הסיבה שצריך שני interface שונים, היא כדי לתמוך בלולאה בתוך לולאה, כי אם המחלקה עצמה הייתה מממשת את IEnumerator במידה ומישהו היה מבצע foreach בתוך foreach הוא לא היה מקבל את התוצאות הרצויות, כעת שכל מופע של foreach מקבל מופע של PersonManagerEnumrator משלו, זה לא יהווה בעייה.
שתי נקודות לסיום.
בפוסט הבא אני אדגים שיטה יותר קצרה לממש foreach וזה בעזרת yield.