foreach מריצה את הגוף שלה פעם אחת לכל איבר באוסף, לפי הסדר, בלי אינדקס לנהל:
פלט:
Welcome, Ava
Welcome, Noah
Welcome, Mia
Total: 255
[C][#][7]
הלולאה עובדת אותו דבר על מערך, List<T>, מחרוזת (טיפוס האיבר char), HashSet<T>, שאילתת LINQ או כל רצף אחר. הטיפוס המוצהר יכול להיות var, מה שנפוץ כשטיפוס האיבר ארוך: foreach (var order in pendingOrders).
מה foreach עושה מאחורי הקלעים
foreach מבקשת מהאוסף enumerator ועוברת איתו. עבור List<int>, הקומפיילר הופך את הלולאה בערך לזה:
List<int>.Enumerator e = scores.GetEnumerator();
try
{
while (e.MoveNext())
{
int score = e.Current;
// loop body
}
}
finally
{
e.Dispose();
}
נובעות מזה שלוש השלכות. כל טיפוס עם מתודה ציבורית GetEnumerator() עובד, ובדרך כלל היא מגיעה ממימוש של IEnumerable<T>. ה-enumerator משוחרר גם כשהגוף זורק חריגה או מבצע break. ומשתנה האיבר הוא עותק חדש של e.Current בכל סיבוב, ולכן השמה אליו לא יכולה לשנות את האוסף.
אי אפשר להשתמש בטיפוס בלי GetEnumerator:
error CS1579: foreach statement cannot operate on variables of type 'Order' because 'Order' does not contain a public instance or extension definition for 'GetEnumerator'
ממשו IEnumerable<T> על הטיפוס, או עברו על האוסף שהוא מכיל (order.Lines במקום order). אפשר לכתוב רצפים משלכם עם yield return, שמוסבר בעמוד על IEnumerable ו-yield.
מעבר על Dictionary
האיברים של מילון הם ערכי KeyValuePair<TKey, TValue> עם המאפיינים Key ו-Value:
פלט:
apples: 12
pears: 0
kiwis: 30
<apples><pears><kiwis>
Units in stock: 42
30 kiwis
12 apples
0 pears
Dictionary לא מבטיח שום סדר מעבר. בפועל הוא עוקב אחרי סדר ההכנסה עד שמוחקים רשומות, אבל קוד שתלוי בסדר צריך למיין במפורש, כמו שהלולאה האחרונה עושה עם OrderByDescending, או להשתמש ב-SortedDictionary.
ב-.NET Core 2.0 ואילך (ובכל .NET מודרני), ל-KeyValuePair יש מתודת Deconstruct, כך שאפשר לפרק את הזוג לשני משתנים ישירות בכותרת הלולאה:
// modern .NET: KeyValuePair deconstruction
foreach (var (fruit, count) in stock)
{
Console.WriteLine($"{fruit}: {count}");
}
זו הצורה הקצרה ביותר, וזו שתראו בקוד עדכני. היא לא מתקמפלת ב-.NET Framework, שבו ל-KeyValuePair אין Deconstruct; שם השתמשו ב-item.Key וב-item.Value.
קבלת האינדקס
ל-foreach אין אינדקס משלה. שתי הדרכים הרגילות לקבל אחד הן מונה שאתם מתחזקים, או העמסה של Select ב-LINQ שמעבירה את האינדקס יחד עם כל איבר:
פלט:
1. Kenji
2. Sofia
3. Omar
index 0: Kenji
index 1: Sofia
index 2: Omar
המונה הוא הזול והברור ביותר. אם אתם מוצאים את עצמכם משתמשים באינדקס כדי לקרוא podium[i - 1] או לכתוב podium[i] = ..., עברו ללולאת for, שנבנתה בדיוק בשביל זה.
משתנה הלולאה הוא לקריאה בלבד
אי אפשר לבצע השמה למשתנה האיטרציה:
foreach (string name in names)
{
name = name.Trim();
// error CS1656: Cannot assign to 'name' because it is a 'foreach iteration variable'
}
וגם אם זה היה מותר, זה היה משנה רק עותק מקומי. כדי לשנות כל איבר, בנו רצף חדש (names.Select(n => n.Trim()).ToArray()) או השתמשו בלולאת for שכותבת names[i] = names[i].Trim().
מה שכן אפשר לעשות תלוי בשאלה אם האיבר הוא class או struct. עבור class, המשתנה מחזיק הפניה, ולכן שינוי של מאפיין משנה את האובייקט שבאוסף:
פלט:
Order 1: shipped
Order 2: shipped
אם Order היה struct, אותה השמה הייתה נכשלת עם CS1654 (Cannot modify members of 'order' because it is a 'foreach iteration variable'), כי המשתנה הוא עותק של הערך ולא הפניה אליו.
שינוי האוסף: InvalidOperationException
הוספה או מחיקה של איברים ב-List או ב-Dictionary בזמן ש-foreach עוברת עליו שוברת את ה-enumerator. ה-MoveNext הבא זורק InvalidOperationException עם ההודעה "Collection was modified; enumeration operation may not execute.":
פלט:
InvalidOperationException: collection was modified
milk, bread
milk, bread
הפתרונות, מהפשוט לגמיש ביותר:
RemoveAll(predicate)עלList<T>מוחק כל התאמה במעבר אחד.- מעבר על עותק עם
ToList()אוToArray(), ואז אפשר לשנות את המקור בחופשיות. - קודם לאסוף, אחר כך להחיל: אספו את המפתחות למחיקה ברשימה נפרדת במהלך הלולאה, ומחקו אותם אחריה. זו התבנית הרגילה עבור
Dictionary. - לולאת
forמהסוף להתחלה עםRemoveAt(i).
ל-Dictionary יש חריג אחד ששווה להכיר. מאז .NET Core 3.0, דריסה של הערך של מפתח קיים (stock[key] = 0) או מחיקת רשומות במהלך foreach עליו כבר לא זורקות חריגה, בעוד שהוספת מפתח חדש עדיין זורקת. ב-.NET Framework שלושתן זורקות, ולכן איסוף המפתחות מראש הוא הגרסה שעובדת בכל מקום.
break, continue ואוספים שהם null
break ו-continue מתנהגים כמו בכל לולאה אחרת:
פלט:
reading 21
reading 23
no tags, no crash
מעבר על אוסף שהוא null זורק NullReferenceException כשקוראים ל-GetEnumerator. ?? Enumerable.Empty<T>() הופך רצף שעשוי להיות null לרצף ריק; עדיף עוד יותר להחזיר אוספים ריקים במקום null מהמתודות שלכם.
foreach מול List.ForEach
ל-List<T> יש מתודה ForEach(Action<T>): names.ForEach(n => Console.WriteLine(n));. היא לא זהה ללולאה. הגוף שלה הוא lambda, ולכן break, continue ו-yield לא זמינים ו-return מסיים רק את הקריאה עבור האיבר הנוכחי. היא קיימת רק ב-List<T>, לא במערכים או ברצפים אחרים. העדיפו את הפקודה foreach; השתמשו ב-ForEach רק לפעולה של שורה אחת שכבר יש לכם כמתודה.
שאלות נפוצות
איך עוברים על Dictionary עם foreach ב-C#?
כל איבר הוא KeyValuePair<TKey, TValue>: foreach (var pair in prices) Console.WriteLine($"{pair.Key}: {pair.Value}");. ב-.NET Core 2.0 ואילך אפשר לפרק אותו: foreach (var (name, price) in prices). כדי לעבור רק על המפתחות או רק על הערכים, השתמשו ב-prices.Keys או ב-prices.Values.
איך מקבלים את האינדקס בלולאת foreach ב-C#?
ל-foreach אין אינדקס מובנה. החזיקו מונה (int i = 0; לפני הלולאה, i++ בסוף הגוף), או הטילו כל איבר יחד עם האינדקס שלו בעזרת LINQ: foreach (var item in names.Select((name, index) => new { name, index })). אם צריך את האינדקס כדי לכתוב בחזרה או להשוות שכנים, לולאת for מתאימה יותר.
מה המשמעות של "Collection was modified; enumeration operation may not execute"?
הוספתם או מחקתם פריטים ב-List, ב-Dictionary או באוסף אחר בזמן ש-foreach עברה עליו. ה-enumerator מזהה את השינוי וזורק InvalidOperationException בצעד הבא. עברו על עותק (foreach (var x in list.ToList())), אספו את השינויים והחילו אותם אחרי הלולאה, השתמשו ב-list.RemoveAll(...), או השתמשו בלולאת for מהסוף להתחלה.
למה אי אפשר לבצע השמה למשתנה של foreach ב-C#?
משתנה האיטרציה הוא לקריאה בלבד: name = name.Trim(); בתוך foreach (string name in names) היא שגיאה CS1656. השמה אליו ממילא לא הייתה משנה את האוסף. כדי להחליף איברים, השתמשו בלולאת for עם אינדקס, או בנו אוסף חדש עם Select. שינוי של מאפיין באובייקט של מחלקה (order.Status = ...) מותר.
אפשר לצאת מלולאת foreach באמצע ב-C#?
כן. break מסיים את הלולאה מיד ו-continue מדלג לאיבר הבא, בדיוק כמו ב-for וב-while. return יוצא מכל המתודה. המתודה List<T>.ForEach שונה: הגוף שלה הוא lambda, ולכן break ו-continue לא זמינים בה.