לדלג לתוכן

פונקציות סדרות וחיים תלויים

דליפה במערכת מנוהלת יכולה להתחיל בשורה שנראית כמו חיבור נוח לאירוע. כל עוד המפרסם מחזיק delegate, ה־delegate עשוי להחזיק subscriber, וה־subscriber עשוי להחזיק buffers גדולים. ה־GC רואה גרף תקין ונגיש; הוא אינו יודע שכבר סגרנו את המסך ושאין לנו צורך בנתונים. נחקור את ה־references שנוצרים מפונקציות וסדרות.

הדוגמה הבאה יוצרת סגירה על משתנה ומשנה אותו אחרי יצירת הפונקציה.

int threshold = 10;
Func<int, bool> accept = value => value >= threshold;
threshold = 20;
Console.WriteLine(accept(15));

var checks = new List<Func<int>>();
for (int i = 0; i < 3; i++) checks.Add(() => i);
Console.WriteLine(string.Join(",", checks.Select(f => f())));

הפלט הראשון הוא False והסדרה הבאה מכילה שלוש פעמים 3. הסגירה תופסת משתנה, ולא snapshot של הערך שלו בזמן יצירת ה־delegate. בלולאת for קיים משתנה i אחד ששלושת ה־delegates קוראים. נוכל ליצור עותק מקומי בתוך גוף הלולאה ולסגור עליו. לעומת זאת lambda שמסומנת static אינה רשאית לתפוס מצב מן ההקשר, ולכן היא כלי לתיעוד ולמניעת לכידה מקרית.

נוסיף עכשיו סדרה דחויה. זוהי תוכנית נפרדת שמדגימה מתי באמת מתבצעת עבודה.

int inspected = 0;
IEnumerable<int> query = Enumerable.Range(1, 5)
    .Where(value => { inspected++; return value % 2 == 0; });
Console.WriteLine($"before={inspected}");
Console.WriteLine(query.Count());
Console.WriteLine(query.Sum());
Console.WriteLine($"after={inspected}");

ה־Where מחזיר מתכון למעבר. שתי פעולות הצבירה עוברות עליו בנפרד, ולכן inspected מגיע לעשר. אם המקור היה קורא קובץ או שה־predicate היה פונה למערכת אחרת, חזרה על המעבר הייתה גוררת פעולות נוספות ותוצאות שעשויות להשתנות. השימוש ב־ToArray מבצע את העבודה ומקבע את התוצאה בנקודה מסוימת, במחיר הקצאה ובעלות על snapshot. לפני שנוסיף אותו בכל מקום נבדוק אם באמת נדרשים כמה מעברים.

ה־iterator שנכתב באמצעות yield return מתורגם למכונת מצבים. הפרמטרים והמשתנים שצריכים לשרוד בין צעדים עשויים להפוך לשדות באובייקט. ה־MoveNext מקדם את החישוב, וה־Dispose של ה־enumerator מאפשר להריץ finally גם כשיצאנו מן הלולאה לפני סוף הסדרה. שימוש ידני ב־GetEnumerator בלי Dispose עלול להשאיר משאב פתוח יותר מן המצופה. הלולאה foreach דואגת ל־disposal המתאים כחלק מהתרגום שלה.

בחיבור לאירוע נשתמש באותו delegate גם לניתוק. שתי lambdas זהות מבחינה טקסטואלית אינן חוזה של זהות שניתן לבנות עליו. מחלקת Subscriber תוכל לשמור handler בשדה, להירשם בבנאי ולהסיר אותו ב־Dispose. אם ה־publisher מאריך ימים מן ה־subscriber, זאת פעולת cleanup שמונעת שורש חי בלתי נחוץ. בינתיים החוזה שלנו חד־תהליכי; ניתוק אינו מבטיח שאף callback לא רץ במקביל, ואת הבעיה הזאת נפתור בפרק הבא.

הבדיקה הנכונה לדליפה תצייר מסלול: publisher אל invocation list, משם אל closure או instance target, ומשם אל הנתונים. החזקה של WeakReference בודקת נגישות בלי להפוך בעצמה לשורש חזק, אבל ניסוי GC דורש להוציא locals מן ה־stack בצורה מבוקרת. אל תסיקו מאיסוף יחיד שלא קרה שיש דליפה ודאית.

התיעוד על ביצוע דחוי ועל רישום לאירועים וניתוק מהם מתאר את החוזים שעליהם נבנה. המטרה אינה להימנע מ־LINQ או מ־events, אלא לדעת איזה חישוב נשמר ואיזה גרף נשאר חי.