כל לולאת foreach וכל שאילתת LINQ עובדות דרך ממשק אחד קטן: IEnumerable<T>. הבנה שלו מסבירה למה LINQ עצל, למה אפשר לקרוא חלק מהרצפים רק פעם אחת, ואיך yield return מאפשר לכתוב רצפים משלכם עם לולאה פשוטה.
מה זה IEnumerable
ל-IEnumerable<T> יש מתודה אחת, GetEnumerator(). ל-enumerator שהיא מחזירה יש MoveNext() (להתקדם ולדווח אם יש איבר) ו-Current (האיבר). foreach היא קיצור להפעלה של ה-enumerator הזה:
פלט:
red green blue
red green blue
ה-enumerator מתחיל לפני האיבר הראשון, ולכן ה-MoveNext() הראשון עובר אליו. כש-MoveNext() מחזיר false, הרצף נגמר. בלוק ה-using משחרר את ה-enumerator, וזה חשוב עבור iterators שמחזיקים משאבים כמו קבצים פתוחים.
IEnumerable מול ICollection מול List
ממשקי האוספים יוצרים סולם. כל שלב מבטיח יותר:
| טיפוס | מוסיף | שימוש טיפוסי |
|---|---|---|
IEnumerable<T> | רק foreach | פרמטרים שרק עוברים עליהם; תוצאות LINQ; רצפים עצלים |
IReadOnlyCollection<T> | Count | ערכי החזרה שמי שקורא לא אמור לשנות |
ICollection<T> | Count, Add, Remove, Contains | קוד שמוסיף לאוסף מכל סוג |
IReadOnlyList<T> | גישה לפי אינדקס [i] | תוצאות לקריאה בלבד עם מיקומים |
IList<T> | גישה לפי אינדקס, Insert, RemoveAt | קוד שעורך לפי מיקום |
List<T> | המחלקה הקונקרטית שמממשת את כל אלה | אחסון |
מתודה שרק עוברת על הקלט שלה צריכה לקבל IEnumerable<T>, כדי שמי שקורא יוכל להעביר מערך, רשימה, קבוצה או שאילתת LINQ בלי להמיר. מתודה שמחזירה נתונים צריכה בדרך כלל להחזיר משהו שאומר "זה כבר בזיכרון", כמו List<T> או IReadOnlyList<T>, כי ערך החזרה מסוג IEnumerable<T> יכול להיות שאילתה שעושה עבודה בכל פעם שקוראים אותה.
פלט:
30
15
4.0
30
כתיבת iterator עם yield return
כדי להפיק רצף משלכם, כתבו מתודה שמחזירה IEnumerable<T> והשתמשו ב-yield return לכל איבר. הקומפיילר הופך את המתודה למחלקה שמממשת בשבילכם את ה-enumerator.
פלט:
0, 2, 4, 6, 8, 10
Mon Tue Wed Thu Fri
אין שום רשימה ב-EvenNumbers. כל yield return מוסר ערך אחד למי שקרא ומשהה את המתודה, ושומר על המשתנים המקומיים שלה (כאן i) בחיים עד שמי שקרא מבקש את הערך הבא.
עצלות: שימו לב לסדר הביצוע
מכיוון שהמתודה משהה את עצמה, הגוף שלה והלולאה של מי שקרא מתחלפים בתורות. הדפסה משני הצדדים מראה את השזירה:
פלט:
Called Numbers(), nothing printed yet
start
yielding 1
Loop got 1
resumed after 1
yielding 2
Loop got 2
resumed after 2
yielding 3
Loop got 3
resumed after 3
end
הקריאה ל-Numbers() לא הדפיסה "start": היא רק יצרה את ה-enumerable. הגוף התחיל ב-MoveNext() הראשון בתוך ה-foreach, רץ עד ה-yield return הראשון ועצר. כל צעד מאוחר יותר המשיך מהשורה שאחרי ה-yield. Where ו-Select של LINQ מתנהגות באותה צורה (סביבת הריצה מממשת אותן עם מחלקות enumerator שנכתבו ביד לטובת מהירות, אבל האפקט זהה), ולכן שאילתות LINQ נדחות.
yield break
yield break מסיים את הרצף מוקדם, הגרסה של return ב-iterator:
פלט:
Subject: Invoice
From: shop@example.com
אי אפשר לערבב return value; ו-yield return באותה מתודה. ברגע שמתודה מכילה yield, היא iterator, ו-yield break היא הדרך היחידה לצאת ממנה מוקדם.
רצפים אינסופיים
מכיוון שמחושבים רק האיברים שמבקשים, iterator יכול לתאר רצף בלי סוף. הצרכן מחליט כמה לקחת:
פלט:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34
1597
2026-09-28 Mon
2026-10-05 Mon
2026-10-12 Mon
while (true) בטוח כאן כי Take ו-First מפסיקות לבקש אחרי שיש להן את מה שהן צריכות. לעומת זאת, קריאה ל-ToList() או ל-Count() על רצף אינסופי לא חוזרת לעולם, וכך גם מעבר על OrderBy שלו, כי מיון צריך קודם את כל האיברים.
המלכודות של רצפים עצלים
בדיקות ארגומנטים רצות מאוחר. כל הגוף של iterator נדחה, כולל הבדיקות שבראשו. הפתרון הוא לפצל את המתודה: מתודה רגילה בודקת את הארגומנטים ומחזירה את התוצאה של iterator פרטי.
פלט:
LazyRange(-1) returned without an error
...and threw only when enumerated
EagerRange(-1) threw at the call
כל מעבר מריץ את המתודה מחדש. iterator לא שומר שום דבר במטמון. מעבר כפול על Numbers() מדפיס "start" פעמיים, קורא את הקובץ פעמיים, או קורא לשירות ה-web פעמיים. סביבות פיתוח מסמנות את זה כ-"possible multiple enumeration of IEnumerable". קראו ל-ToList() פעם אחת כשצריך את האיברים יותר מפעם אחת.
finally רץ כשהצרכן עוצר. אם מי שקרא יוצא מה-foreach מוקדם, ה-enumerator משוחרר וכל בלוק finally ב-iterator רץ, וכך iterator שקורא קובץ סוגר אותו:
פלט:
open sensor
10
20
30
close sensor
done
כמה כללים שהקומפיילר אוכף על iterators: yield return לא יכול להופיע בתוך בלוק try שיש לו catch (try/finally זה בסדר), בתוך lambda או מתודה אנונימית, או במתודה עם פרמטרים ref או out.
Async streams
לרצפים שהאיברים שלהם מגיעים באופן אסינכרוני (עמודים מ-API, שורות ממסד נתונים), C# 8 ואילך (עם .NET Core 3.0 ואילך) משלבת iterators עם await דרך IAsyncEnumerable<T>:
static async IAsyncEnumerable<string> FetchPages()
{
for (int page = 1; page <= 3; page++)
{
await Task.Delay(100); // simulate a network call
yield return $"page {page}";
}
}
await foreach (string p in FetchPages())
{
Console.WriteLine(p);
}
הצרכן משתמש ב-await foreach, ומחכים לכל איבר כשהוא מופק.
טעויות נפוצות
- החזרת
IEnumerable<T>משאילתה וקריאה שלו הרבה פעמים. כל קריאה מריצה אותו מחדש; החזירו במקום זאתList<T>. - בדיקת ארגומנטים בתוך iterator. הבדיקה נדחית; פצלו למתודה עוטפת ול-iterator.
- קריאה ל-
Count()או ל-ToList()על iterator אינסופי. זה לא נגמר לעולם; קודםTake. - ציפייה שלפרמטר
IEnumerable<T>יהיוCountאו[i]. השתמשו ב-Count()של LINQ (שעלולה לעבור על הרצף) או קבלוIReadOnlyList<T>.
שאלות נפוצות
מה זה IEnumerable ב-C#?
IEnumerable<T> הוא הממשק של כל דבר שאפשר לעבור עליו עם foreach. יש לו מתודה אחת, GetEnumerator(), שמחזירה IEnumerator<T> עם MoveNext() ו-Current. מערכים, רשימות, מילונים, קבוצות, שאילתות LINQ ומתודות iterator כולם מממשים אותו.
מה ההבדל בין IEnumerable ל-List ב-C#?
IEnumerable<T> מבטיח רק שאפשר לעבור עליו; אין לו מאפיין Count, אין אינדקסר ואין Add, ויכול להיות שהוא מחושב בעצלות בכל פעם שעוברים עליו. List<T> הוא אוסף קונקרטי ששמור בזיכרון עם כל אלה. קבלו IEnumerable<T> בפרמטרים כשרק עוברים בלולאה, והחזירו List<T> או IReadOnlyList<T> כשמי שקורא צריך אינדקס או ספירה.
מה yield return עושה ב-C#?
yield return value; בתוך מתודה שמחזירה IEnumerable<T> מוסר איבר אחד למי שקרא ומשהה את המתודה בנקודה הזו. בפעם הבאה שמי שקרא מבקש איבר, המתודה ממשיכה מהשורה הבאה. הקומפיילר כותב מחדש את המתודה כמכונת מצבים, כך שאתם כותבים לולאה ומקבלים רצף עצל.
מה זה yield break ב-C#?
yield break; מסיים את הרצף: ה-foreach של מי שקרא מסתיים כרגיל אחרי האיברים שכבר הופקו. זו המקבילה של return ב-iterator, ומשתמשים בה כדי לעצור מוקדם, למשל כשמגיעים למגבלה או כשהקלט ריק.
למה מתודת ה-iterator שלי לא זורקת חריגה כשאני קורא לה?
הגוף של מתודת iterator לא רץ עד שעוברים על הרצף, ולכן בדיקות ארגומנטים בראש המתודה רצות ב-MoveNext הראשון ולא בזמן הקריאה. כדי לבדוק מיד, שימו את הבדיקות במתודה ציבורית רגילה שקוראת אחר כך למתודת iterator פרטית שמחזיקה את פקודות ה-yield.