מבוא
אחד החסרונות הבולטים בצורת ניהול הזכרון של דוט-נט הוא שאנחנו אף פעם לא
יודעים באמת כמה זמן האובייקטים שלנו חיים, ומתי בפועל משחררים אותם
מהזכרון. מכאן, שאין לנו אפשרות להשתמש ב-Destructor שמופעל תמיד ברגע
שאנחנו רוצים, בקונטקסט שמתאים לנו, ובזמן שנוח לנו. אז במקום Destructor,
קיבלנו Finalizer. שלמעשה מדובר גם כן בפונקציה שגם כן מופעלת "רגע לפני"
שה-GC מחליט לשחרר את האובייקט שלנו. בתוך גוף ה-Finalize היינו יכולים
להכניס קוד שיטפל בכל מיני "סגירת פינות" לפני שהאובייקט מת, למשל שחרור
משאבים שנמצאים מחוץ לגבולות התוכנית שלנו (Streams, Sockets,
DBConnections וכו'...). אבל בפועל, זה לא מומלץ משתי סיבות. הראשונה, היא
שמאחר ואנחנו לא באמת יודעים מתי יפעילו את Finalize (ובפועל, אף אחד גם
לא מבטיח לנו ב-100% שבאמת יקראו לו), לכן, מעט קשה ומסוכן לנהל משאבים
שיכולים להיות נגישים גם לתהליכים שרצים במקביל לתוכנית שלנו. השניה, היא
שהפעלת ה-Finalizer'ים יכולה לגזול זמן יקר, לכן כברירת מחדל ה-Finalizer
יופעל רק אם הענקנו לו מימוש כלשהו.
הקו המנחה שלנו צריך להיות שאם סיימנו להשתמש בו - האחריות שלנו היא לשחרר אותו.
כאן בדיוק נכנס לתמונה הממשק
IDisposable,
שבא לפתור לנו בדיוק את הבעיה הזאת. ובמשפט אחד, שימוש ב-IDisposable
מעניק לנו דסטרקטור דטרמיניסטי. במקום לחכות עד שה-GC יחליט לשחרר את
האובייקט, אנחנו יכולים לקרוא ל-Dispose ברגע שסיימנו לעבוד עם האובייקט
ולהפעיל את לוגיקת "סגירת הפינות" שלנו, בלי שנצטרך להוסיף עבודה מיותרת
ב-Finalization Queue.
בנוסף לכך, אנחנו מרוויחים על הדרך הזדמנות להסיר הרשמויות מאירועים אליהם
האובייקט שלנו נרשם בעבר. מאחר וכל עוד האובייקט שלנו רשום לאירועים אצל
אובייקטים אחרים, אנחנו למעשה יוצרים תלות ביניהם, מה שגורם ל-GC להמנע
מלשחרר אותם (אחרי הכל, מדובר בעוד רפרנס לאובייקט). זאת נקודה די רגישה
שבמקרים מסויימים עלולה לגרום לסוג של "דליפת זכרון" (או לפחות בגרסה
הדוט-נטית שלה). לכן הסרת ההרשמויות ב-Dispose יכולה לזרז את שחרור
האובייקט על ידי ה-GC.
ממה להזהר?
בעקרון, ישנן 2 דרכים לממש את IDisposable, מה שרוב האנשים מכירים בתור "הזריזה" ו-"הטרחנית".
במימוש "הזריז" אנחנו מוסיפים את הממשק למחלקה שלנו, מכניסים את הקוד שלנו לתוך הגוף של Dispose, וסיימנו.
במימוש "הטרחני" אנחנו עושים עבודה מעט יותר מסודרת ונכונה. אנחנו למעשה
מבטחים את עצמנו, שבמידה והמפתח שכח לקרוא ל-Dispose, אז אנחנו נהיה מספיק
חכמים לקרוא ל-Dispose דרך ה-Finalizer. בפועל, מדובר בסוג של באג מאחר
וזאת אחריות המפתח לקרוא ל-Dispose בעצמו, לכן הנטייה שלי היא לזרוק שגיאה
במקרים כאלה, ואם לא אז לפחות לכתוב ללוג על התקרית הבעייתית הזאת. מימוש
לפי התבנית הזאת היא למעשה צורת העבודה הנכונה והמומלצת, מאחר וכשמדובר
בניהול משאבים, זה לא בדיוק המקום להתחיל להתחכם ולעגל פינות בקוד. מידע
נוסף אפשר למצוא
כאן.
הבעיה היא, שהרבה פעמים מפתחים נוטים לממש את אותו Design Pattern פופולרי
בצורה עיוורת, בלי באמת להבין למה, או מה המשמעות של הקוד הזה.
אני אתן דוגמא לשגיאה.. פופולרית למדי, שנובעת מחוסר ההבנה הזה בדיוק.
כאמור, תחת המימוש של אותה תבנית מוכרת של IDisposable, יכולים להפעיל את
Dispose דרך 2 קונטקסטים נפרדים. במקרה אחד, המשתמש יכול לקרוא לה.
בקונטקסט הזה, אנחנו בטוחים. אנחנו יודעים שהאובייקט שלנו חי וקיים,
ולמעשה אנחנו יכולים לעשות מה שבא לנו בתוך הפונקציה. להבדיל מהמקרה הזה,
הסביבה יכולה גם להפעיל את Dispose דרך ה-Finalizer (במידה והמשתמש לא קרא
בעצמו ל-Dispose או חס וחלילה לא הפעיל את
GC.SupressFinalize). עכשיו, בקונטקסט הזה אנחנו הרבה יותר
מוגבלים.
בגלל שאנחנו מופעלים דרך ה-Finalizer, אנחנו למעשה נמצאים באיזשהו
מצב-ביניים בכל הנוגע לחיות (Liveliness) האובייקט שלנו. מצד אחד,
האובייקט שלנו "חי", אבל לא מובטח לנו שכל ה-Managed Data Members שאנחנו
מחזיקים אליהם רפרנס מתוך האובייקט באמת קיימים עדיין. כלומר, יתכן שעד
שהספיקו להפעיל את ה-Finalize שלנו, ה-GC כבר "נתן עבודה" והספיק לקרוא ל-Finalize של ה-data member'ים השונים שלנו (במידה והם ממשים אותם). כך, שבאופן עקרוני, במידה ואנחנו יודעים באופן וודאי כי ה-member'ים שלנו לא משתמשים ב-Finalize כדי לשחרר משאבים פנימיים, נוכל להשתמש בהם באופן בטוח גם מתוך הקונטקסט של Finalize. אבל, מאחר ואנחנו אף פעם לא יכולים להיות בטוחים ב-100% לגבי השאלה הזאת, עדיף שלא לעשות את ההנחות המסוכנות הללו, ופשוט לא לגשת ל-Managed Types דרך ה-Finalizer שלנו. לעומת זאת, אין סיבה שלא נפנה ל-Native Types גם בקונטקסט של Finalize.
במימוש נכון של IDisposable, אנחנו נבדוק באיזה קונטקסט אנחנו נמצאים ועל
פי זה נבחר לאיזה אובייקטים ניגש. הבאג הזה פופולרי במיוחד בגלל שהרבה
פעמים הקוד שנמצא בתוך Dispose למעשה פונה לאובייקטים דוט-נטים אחרים
ומפעיל את Dispose עליהם. וכאמור, כל קריאה ל-Managed Object תחת קונטקסט
של Finalize גובל בפשע. אז עדיף שלא, פשוט לא נעשה את זה.
למען עולם קצת יותר טוב..