Menu

גנרטורים ב-PHP: yield, yield from וחיסכון בזיכרון

גנרטור הוא פונקציה שמשתמשת ב-yield כדי להחזיר ערכים אחד אחד, כך ש-foreach יכולה לעבור על מיליון פריטים בלי לבנות מערך. למדו את yield, מפתחות, חיסכון בזיכרון, רצפים אינסופיים, yield from, send() ו-getReturn().

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

גנרטור הוא פונקציה שמכילה yield. קריאה לה לא מריצה את הגוף; היא מחזירה אובייקט Generator ש-foreach יכולה לעבור עליו. כל yield $value מוסר ערך אחד ללולאה ומשהה את הפונקציה עד שהלולאה מבקשת את הבא, כך שהערכים מופקים אחד אחד במקום להיאסף קודם במערך.

הפלט מתחלף בין הגנרטור ללולאה: כל ערך נוצר רק כשהלולאה מבקשת אותו.

גנרטורים חוסכים זיכרון

range(1, 1000000) בונה מערך של מיליון מספרים שלמים לפני שהלולאה מתחילה. גנרטור שמחזיר את אותם מספרים אף פעם לא מחזיק יותר מאחד מהם. הריצו והשוו את הזיכרון שכל גרסה צריכה:

שתי הלולאות מדפיסות את אותו סכום, 500000500000, אבל גרסת המערך מדווחת extra memory 16.0 MB והגנרטור extra memory 0.0 MB. הפער גדל עם הנתונים. אותו רעיון הופך גנרטורים לשימושיים לקבצים גדולים, לתוצאות ממסד נתונים ולדפים של API: לעבד פריט אחד, לזרוק אותו, לקחת את הבא.

yield עם מפתחות וערכים

yield $key => $value קובע את המפתח ש-foreach ($gen as $key => $value) מקבלת. בלי מפתח, גנרטור ממספר את הערכים שלו מ-0, כמו רשימה. שימוש נפוץ הוא קריאת קובץ שורה אחר שורה עם מספרי שורות:

רק שורה אחת נמצאת בזיכרון בכל רגע, ולכן אותה פונקציה מטפלת בקובץ לוג של 2 GB. fopen() ו-fgets() מסבירות את פונקציות הקבצים שבשימוש כאן.

רצפים אינסופיים וצינורות עצלים

גנרטור יכול לרוץ בלולאה לנצח, כי הוא רץ רק עד כמה שהצרכן מבקש. עצרו עם break, או עטפו אותו בגנרטור אחר שלוקח את הערכים הראשונים. שרשור של גנרטורים קטנים נותן צינור שבו כל שלב עצל:

fibonacci() אף פעם לא נגמרת בעצמה, ובכל זאת התוכנית מסתיימת: take() מפסיקה לבקש אחרי הכמות שהתבקשה, ושום דבר מעבר לזה לא מחושב.

yield from: האצלה לגנרטור אחר

yield from מעביר הלאה כל ערך של מערך, של איטרטור או של גנרטור אחר. כך מפצלים גנרטור לגנרטורים קטנים יותר, או עוברים רקורסיבית על מבנה מקונן:

yield from שומר על המפתחות הפנימיים, ולכן שני המערכים מחזירים את המפתחות 0 ו-1. iterator_to_array() דורסת אז את a ואת b. העבירו false כארגומנט השני בכל פעם שרק הערכים חשובים.

send() ו-getReturn()

גנרטור יכול גם לקבל ערכים. $x = yield; משהה ומחכה; $gen->send($value) ממשיך את הגנרטור כש-$value היא התוצאה של אותו yield. וגנרטור יכול לעשות return לערך סופי, שקוראים עם getReturn() אחרי שהוא הסתיים.

רוב הקוד אף פעם לא צריך send(); קריאה עצלה עם foreach היא השימוש היומיומי. זו אבן הבניין שמאחורי ספריות של coroutines.

אפשר לעבור על גנרטור פעם אחת בלבד

גנרטורים רצים קדימה ואי אפשר להחזיר אותם להתחלה. ברגע ש-foreach צרכה גנרטור, לולאה שנייה על אותו אובייקט זורקת חריגה:

התיקון הוא לקרוא שוב לפונקציית הגנרטור לכל מעבר, foreach (colors() as $c), או לאסוף את הערכים פעם אחת עם iterator_to_array(colors(), false) כשאתם באמת צריכים אותם פעמיים. אם אתם מוצאים את עצמכם עושים את זה לעיתים קרובות, מערך רגיל היה הבחירה הנכונה לנתונים האלה.

שאלות נפוצות

מה yield עושה ב-PHP?

yield $value מוסר ערך אחד לקוד שעובר על הפונקציה בלולאה ומשהה את הפונקציה בדיוק שם. האיטרציה הבאה של הלולאה ממשיכה אותה אחרי ה-yield. כל פונקציה שמכילה yield מחזירה אובייקט Generator במקום לרוץ כשקוראים לה.

למה להשתמש בגנרטור במקום במערך?

מערך מחזיק כל איבר בזיכרון בבת אחת; גנרטור מפיק איבר אחד בכל פעם. לולאה על range(1, 1000000) מקצה את כל מיליון המספרים השלמים, בעוד גנרטור שמחזיר אותם עם yield שומר על זיכרון קבוע. גנרטורים גם מאפשרים רצפים אינסופיים ומפסיקים לעבוד מוקדם כשהלולאה עוצרת.

מה זה yield from ב-PHP?

yield from $source מעביר הלאה כל ערך של גנרטור אחר, של מערך או של Traversable, כאילו הגנרטור הנוכחי החזיר אותם בעצמו. הביטוי גם מקבל את ערך ההחזרה של הגנרטור הפנימי.

אפשר לעבור על גנרטור פעמיים?

לא. גנרטור רץ רק קדימה. אחרי ש-foreach הסתיימה, לולאה נוספת זורקת Exception: Cannot traverse an already closed generator. קראו שוב לפונקציית הגנרטור כדי לקבל גנרטור חדש, או שמרו את הערכים עם iterator_to_array() אם אתם צריכים אותם פעמיים.

איך ממירים גנרטור למערך?

iterator_to_array($gen) אוספת את כל הערכים ושומרת על המפתחות של הגנרטור. אם המפתחות חוזרים, למשל אחרי yield from, ערכים מאוחרים דורסים מוקדמים, אז העבירו false כארגומנט השני כדי לקבל רשימה פשוטה: iterator_to_array($gen, false).

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

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

להתחיל