עבור לתוכן

בעיה בC# ברקורסיה, לא מקבל את הקוד (שגיאת ריצה)

Featured Replies

פורסם

שטויות. אין סיבה שתהיה רק יציאה אחת מהפונקציה.

בטח שיש! ככה לימדו אותי בממר"מ לפני שבע שנים! וגם אותך אם אני זוכר נכון, לפני קצת יותר משבע שנים ;)

וברצינות - אני לא רואה שום סיבה שתהיה רק יציאה אחת - אולי 25 יציאות זה קצת יותר מדי, אבל 2-3-4 (ואולי יותר אם זה נוח ומתאים) זה ממש בסדר.

פורסם

דיון מעניין, אז ארחיב: (!!!thread hijacking ftw)

כשיש מספר נקודות יציאה מפונקציה, אז רמת הקינון (nesting) נוטה להיות יותר נמוכה, הקוד יותר קריא וברור, וקל יותר לכתוב קוד שקודם בודק שהכל בסדר ורק אז מבצע את הפעולה.

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

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

בשפה כמו C++ אז טכניקות RAII (בגדול שחרור משאבים ב-destructor) יעזרו. ושפות עם garbage collection עוד יותר עוזרות.

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

הנה דוגמא לפונקציה שצריכה להקצות הרבה משאבים, ולבצע פעולה, ולבסוף לשחרר. שימו לב שהקוד ב-C.

ראשית נכתוב את זה בתכנות מבני קלאסי:


int func_structural(int a, int b)
{
int ret_val = ERROR;
int *array1;
int *array2;
FILE* out_file;

if ( a < b )
{
array1 = malloc(a * sizeof(int));
if ( array1 != NULL )
{
array2 = malloc(b * sizeof(int));
if ( array2 != NULL )
{
out_file = fopen("output.txt","wt");
if ( out_file != NULL )
{
/* here we do the actual operation with a, b, array1, array2 and out_file ... */
/* ... */
/* ... end of the actual operation */

fclose(out_file);
ret_val = SUCCESS;
}
else
{
printf("error! can't open output file\n");
}
free(array2);
}
else
{
printf("error! can't alllocate array2\n");
}
free(array1);
}
else
{
printf("error! can't allocate array1\n");
}
}
else
{
printf("error! a must be smaller then b\n");
}
return ret_val;
}

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

עכשיו נכתוב את אותו קוד עם מספר נקודות יציאה (return):


int func_multiple_returns(int a, int b)
{
int *array1;
int *array2;
FILE* out_file;

if ( a >= b )
{
printf("error! a must be smaller then b\n");
return ERROR;
}

array1 = malloc(a * sizeof(int));
if ( array1 == NULL )
{
printf("error! can't allocate array1\n");
return ERROR;
}

array2 = malloc(b * sizeof(int));
if ( array2 == NULL )
{
printf("error! can't allocate array2\n");
free(array1);
return ERROR;
}

out_file = fopen("output.txt","wt");
if ( out_file == NULL )
{
printf("error! can't open output file\n");
free(array2);
free(array1);
return ERROR;
}

/* here we do the actual operation with a, b, array1, array2 and out_file ... */
/* ... */
/* ... end of the actual operation */

fclose(out_file);
free(array2);
free(array1);
return SUCCESS;
}

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

הקוד מחולק לארבע חלקים ברורים: וידוא פרמטרים, הקצאת משאבים, ביצוע הפעולה, ולבסוף - שחרור משאבים.

החסרון בקוד כזה זה שיש לו שכפול של קוד השחרור בכל נקודת יציאה, ויש סכנה גדולה לבאגים אם מוסיפים נקודה חדשה.

לבסוף, נכתוב את הקוד בעזרת 2 נקודות יציאה בלבד - יציאה תקינה ויציאה לא תקינה (למעשה, בקוד תראו שיש נקודת יציאה אחת אמיתית, אבל מבחינה לוגית יש כאן שתי נקודות יציאה).


int func_goto(int a, int b)
{
int ret_val = ERROR;
int *array1 = NULL;
int *array2 = NULL;
FILE* out_file = NULL;

if ( a >= b )
{
printf("error! a must be smaller then b\n");
goto release;
}

array1 = malloc(a * sizeof(int));
if ( array1 == NULL )
{
printf("error! can't allocate array1\n");
goto release;
}

array2 = malloc(b * sizeof(int));
if ( array2 == NULL )
{
printf("error! can't allocate array2\n");
goto release;
}

out_file = fopen("output.txt","wt");
if ( out_file == NULL )
{
printf("error! can't open output file\n");
goto release;
}

/* here we do the actual operation with a, b, array1, array2 and out_file ... */
/* ... */
/* ... end of the actual operation */

ret_val = SUCCESS;

release:
if ( out_file != NULL ) fclose(out_file);
if ( array2 != NULL ) free(array2);
if ( array1 != NULL) free(array1);
return ret_val;
}

שימו לב שלקוד זה יש את כל היתרונות של השיטה הקודמת ללא החסרונות:

* קל להבין מה הקוד עושה. הוא קצר ומחולק לוגית בצורה ברורה.

* אין קינון גדול.

* אין שכפול קוד.

* לכל משאב יש מקום אחד שהוא מוקצה בו ומקום אחד שמשחררים אותו.

* קל מאוד להוסיף משאבים בלי סכנת דליפה: לא של המשאב החדש ולא של משאבים קודמים.

* אפשר לשנות סדר ההקצאות ללא חשש.

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

כשיש GC או לפחות destructors, אז קוד עם הרבה return-ים הוא נפוץ, כי אז לא צריך goto על מנת לשחרר משאבים.

פורסם

חשבתי שכל פעם שכותבים goto חתלתול מת :P

פורסם

בשביל זה יש דברים כמו try..finally.

כשיש, יופי. כשאין...

חשבתי שכל פעם שכותבים goto חתלתול מת :P


10 INPUT "Enter cat:", KITTEN$
20 KILL KITTEN$
30 GOTO 10

ארכיון

דיון זה הועבר לארכיון ולא ניתן להוסיף בו תגובות חדשות.

דיונים חדשים