Menu

IEnumerable ו-yield return ב-C#: רצפים עצלים בהסבר

IEnumerable<T> הוא הממשק שמאחורי foreach ו-LINQ, ו-yield return היא הדרך הקלה ביותר לממש אותו. למדו איך IEnumerable קשור ל-ICollection ול-List, איך מתודת iterator רצה צעד אחרי צעד, yield break, רצפים אינסופיים, והמלכודות של הערכה עצלה.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

כל לולאת 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.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל