על חשיבות destructors (חלק א’?)

אני תמיד פותח באזהרה. דבר ראשון – זהו פוסט מאוד מחשביסטי 🙂

פוסט זה בא בעקבות דיון שהתפתח בתגובות על פוסט קודם. החלטתי שאחד משני דברים קרו. או שאני לא הבנתי חלק מהמגיבים לגבי יכולות שדוט נט/ג’אווה נותנים, או שהם לא הבינו אותי לגבי החשיבות של הדברים.
עדכון – זה היה הראשון. לא יהיה חלק ב’

נושא הדיון הוא destructors, כלומר פונקציות שמטרתן לשחרר את המשאבים שמחלקה משתמשת בה. על פי מיטב הבנתי, הן בג’אווה והן בדוט-נט אין destructors המתבצעים אוטומטית. אפשר להגדיר פונקציה שמחזירה את מה שהמחלקה הקצתה, אבל הקוד חייב לקרוא לפונקציה הזו במפורש. כדי להדגים את הבעיות שיש עם הגישה הזו, לדעתי, החלטתי לדבר בשפה שאינה משתמעת לשתי פנים – קוד. הסיבה שכתבתי חלק א’ היא שהמקרה שנתתי פה הוא מקרה פשוט יחסית, ולא מדגים את הפתולוגיה הבאמת מסובכת שאפשר, לדעתי, להסתבך בה אם אין destructors שרצים אוטומטית.

הנה הקוד (צביעה באדיבות enscript):

#include <stdlib.h>
#include <time.h>

#include <iostream>
// Auto array is a copy of the "auto_ptr" code taken from STL, with the final "delete" replaced by "delete []"
#include "auto_array.h"

// Define a class to keep track of constructions and destructions
class demo {
    static int count;
    int our_number;
public:
    demo() : our_number(++count)
    {
        std::cout<<"Initialized instance "<<our_number<<std::endl;
    }
    ~demo()
    {
        std::cout<<"Destroyed instance "<<our_number<<std::endl;
    }
};

int demo::count(0);

int main()
{
    srand(time(NULL));

    // Generate up to 100 elements
    int num_elements(rand()%100);

    std::cout<<"Will create (and destroy) "<<num_elements<<" instances"<<std::endl;

    auto_array<demo> array(new demo[num_elements]);

    // And then destroy them all

    return 0;
}

auto_array היא מחלקה שאני יצרתי ע”י זה שהעתקתי את הקוד של STL של auto_ptr, אבל שיניתי כל פעם שהוא מריץ את delete להרצה של delete []‎. אם אין לכם כוח לעשות את השינוי הזה בעצמכם, אפשר להוריד את הקובץ שאני השתמשתי בו.

עכשיו תורכם. תראו לי שבאמת זה פשוט במידה סבירה לייצר את אותו הקוד (כלומר – קוד שצריך השמדה מפורשת) בג’אווה/דוט נט.

שחר

מאת

שחר שמש

מייסד–שותף וחבר ועד בתנועה לזכויות דיגיטליות מייסד שותף בעמותת „המקור”. פעיל קוד פתוח. מפתח שפת התכנות Practical

21 תגובות בנושא “על חשיבות destructors (חלק א’?)”

  1. בשפות מנוהלות יש את את הקונספטים הבאים:
    אם אתה יודע שאתה צריך שחרור מיידי של האובייקט, אתה צריך לממש את הממשקIDisposable כמו שרשמתי (ולקרוא ל Dispose כשאתה צריך, או להשתמש ב using). לרוב משתמשים בזה במקרה של ממשק עם אלמנטים לא מנוהלים (מערכת הפעלה, מסדי נתונים וכו’).

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

  2. תיסלחו לי שאני הולך להגיד הרבה שטויות (כי אני סטודנט צעיר שאוהב C ורק השבוע התחלתי ללמוד Java) אבל אני ממש לא מסכים על Finalize() של Java מפני שלא כמו destructor של C++ שרץ תמיד בסיום אובייקט ה Finalize לא חייב לרוץ דוגמה :


    class stuff{
    public String name;
    String default_str = "Unspesifed";

    public stuff()
    {
    name = default_str;
    System.out.println("Creation of stuff object " + name);
    }

    public stuff(String str )
    {
    if ((str == "") || (str == null))
    {
    name = default_str;

    }
    else
    this.name = str;

    System.out.println("Creation of stuff object " + name);
    }
    public void finalize()
    {
    /*
    * This method will be called eather by System.gc() or the Garage Collector (end of work)
    *
    */
    System.out.println("Finalizing this object " + name);
    }
    }
    public class GarbegeCollector {
    public static void CreateObjects()
    {
    /*
    * This will couse the GC to work (?)
    */
    stuff a = new stuff("Object A");
    stuff b = new stuff("Object B");
    }
    public static void main(String[] argc)
    {
    stuff c = new stuff();
    GarbegeCollector.CreateObjects();

    System.gc();//This will not Destroy C but will try to kill all other.

    }
    }

    כפי שניתן לראות מהקוד אפילו שאני קורא ל gc() שזהוא ה Garbage Collector של JAVA זה לא שfinalize ירוץ.

    דוגמה מהחיים האמיתיים ?
    מה לגבי אם אובייקט הוא התקשרות לקובץ (stream) בסיום האובייקט יש לסגור את האובייקט אז נכון שאפשר לבצע פונקציית cleanup() אבל זה לא מחייב כי לא בטוח שאין יותר referance ברמת ה Heap מפני שGC ירוץ מתי שה JVM יגיד לו.

    אינני יודע עדיין את משמעות copy construter ברמה של Java אבל אני די בטוח שאם כמה העתקות של מחרזות ניתן להגיע למגבלות זיכרון .

    נ.ב.
    רוב הסיכויים שאני מדבר שטויות אבל עדיין יש בזה משהוא לא ?

  3. דבר ראשון, זה שאתה מריץ GC לא אומר בהכרח שהוא הגיע להחלטה עדיין לפנות אובייקט מסוים. אם יש לך אובייקט שמתעסק עם STREAMS ודברים לא מנוהלים, כמו שאמרתי, לרוב יש לאובייקט הזה ממשק DISPOSE שאתה צריך לדאוג לקרוא לו ברגע שצריך

  4. finalize אכן לא בהכרח נקרא – ספציפית (וזה פאק נוראי של ג’אווה אם תשאל אותי) אם בסיום התוכנית יש אובייקטים בזכרון, המכונה הוירטואלית מעדיפה פשוט לסיים מאשר לשחרר אותם. הבעיה העיקרית עם החוסר בdestructors בג’אווה (שבה, שלא כמו ב-.net אין אובייקטים “לא מנוהלים”) היא שאם יש לך אובייקט שמייצג פלט דרך buffer (לדוגמה stream לקובץ, אבל לא יכול להיות דברים אחרים) אתה חייב לקרוא ל-flush ידנית מתישהו אחרת אין חובה שהאובייקט יושמד כמו שצריך (ויעשה flush לעצמו) ויש סיכוי שמה שכתבת לא ישמר. בקוד שבו אובייקט הפלט עצמו נמצא מתחת לכמה רמות אבסטרקציה לכתוב את ה-flush האחרון הזה יכול להיות ממש ממש לא נוח.

  5. נכון שבג’אווה finalize לא נקרא בסיום התכנית – אני גם לא יודע למה. בקשר לדוגמא של jabka, יש שתי בעיות:
    1) המכונה הוירטואלית לא מריצה את ה gc כל פעם שאובייקטים משתחררים בסיום של scope, לכן ההערה בקוד לגבי הסיום של createObjects היא לא נכונה.
    2) הקריאה ל system.gc() בסוף של main לא אמורה לשחרר את C, בגלל שיש אליו עדיין קישור (מתוך main עצמה). יש פה מין מלכוד 22, שבגלל שג’אווה לא מריצה את gc בסיום של התכנית, אז אתה לא יכול לשחרר בעצמך אובייקטים שהוגדרו בתוך main.

    שחר, שאלה: למה אתה לא משתמש ב subscribe to comments plugins?

  6. שמתי לב אחרי ששלחתי את ההודעה 🙂
    תיקון טעות קל לגבי המלכוד 22 שהזכרתי קודם. לגבי הדוגמא של jabka, אפשר להוסיף את השורה הזאת:
    c = null;
    לפני הקריאה ל gc, ואז גם c ישוחרר. כמובן שזאת דוגמא פשוטה מדי. יותר סביר שאם יש לך אובייקטים כאלה חשובים שחייבים להיות משוחררים, אז main זה לא המקום הנכון להגדיר אותם.

  7. ממש ממש ממש לא נכון. סמנטית זה עשוי לעשות את אותו הדבר, אבל השניים מאוד רחוקים מלהיות שקולים.

    האיברים במחלקה שלך יאותחלו (באמצעות ה-default constructor, אם לא עשית שום דבר אחר) לפני שה-constructor שלך ירוץ. באותו אופן, בהשמדה של המחלקה, כל מה שה-constructor שלו סיים לרוץ יושמד, מה שלא, לא.

    בצורה הראשונה אתה מאתחל את האיברים באמצעות ה-constructor שלהם. בצורה השניה רץ ה-default constructor שלהם, ואתה אחר כך משנה את תוכנם באמצעות אופרטור ההשמה שלהם.

    מתי זה משנה?
    בכל מקרה שבו האיתחול הוא לא טריויאלי. לרוב המשתנים ה-builtin ה-default constructor לא עושה כלום, ואז זה לא באמת משנה. אם ה-default constructor עושה משהו יותר משמעותי, ייתכן שאתה לא תהיה מעוניין שהוא ירוץ על ריק.

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

    שחר

  8. טוב שאמרת לי את זה, כי לא ידעתי. מסתבר שחיפוש מהיר בגוגל לא נותן תמיד מענה איכותי לשאלות שלך.
    מסתבר גם, שכשאתה מאתחל את המשתנה ע”י השמה, אתה יוצר משתנה זמני, מעביר לתוכו את ערך המשתנה, ומעביר את המשתנה הזמני למשתנה של המחלקה, ובמה שנקרא
    initialization list
    זה קורה בלי משתנה זמני, ולכן זה יותר יעיל
    http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.6
    אני צריך לקרוא על זה עוד.
    אגב, ההבדל הזה הוא בסטנדרט של השפה?
    חידה:
    מה יהיה הפלט של התוכנה הבאה

    int *f(int **x,int *y) {
    *x = y;
    return y;
    }
    int main(int argc,char *argv[]) {
    int *x,*y,xval=2,yval=1;
    x = &xval;
    y = &yval;
    *f(&x,y) = *f(&y,x);
    printf("x points to %d\n",*x);
    return 0;
    }

    האם ייתכן שהשאלה מה להריץ לפני מה קשורה לתשובה לחידה שלי?

סגור לתגובות.