BinaryVision

האם goto באמת שטני?

מאת בתאריך 26/01/09, תחת כללי

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

לאלה מכם שלא יודעים goto זו קפיצה בלתי מותנית ב c (ודומותיה).
דוגמא לשימוש בקוד:

  1. goto exit;
  2. printf("hello world!\n");
  3. exit: return 0;

בקוד הזה, שורה 2 לעולם לא תקרא, שורה 1 תגרום לקפיצה בלתי מותנית (קרי: תמיד) לשורה 3, אשר תסיים את הרצת הפונקציה.


אז למה בעצם goto נחשב רע?

לפי דעתי ישנן כמה סיבות אפשריות:
1. goto נחשב רע עקב רצון שאנשים ישתמשו באלטרנטיבות היותר "high level" שלו, כמו for, while, if, try, etc… ובשביל שאנשים באמת ישתמשו בהם, התחילו להעליל על goto.
הנה לדוגמא קוד ב goto (סטייל אסמבלי) וקוד עם for:

  for (i=0; i<5 ; i++)
  	print("hi");

לעומת הקוד עם goto:

  i=0;
  
  start:
  if (i<5) 
  
  print ("hi");
  
  i++;
  goto start;
  
  exit:

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

  int
  foo (int i) 
  {
  	goto a;	
  	return 0;
  }
  
  int
  bar (int j)
  {
  a:	
  	print("hi");
  	return 0;
  }

3. בעצם אנשים עשו בלאגן עם goto בעבר הרחוק ואז אנשים התרגלו לראות goto ולברוח בזוועה.
לדוגמא:

  while (i<6) {
  start:
  	for (j=0; j<5; j++) {
  		if (j==3)
  			goto start;
  	}
  	i++;
  }

למה כן
כל אחת מהסיבות האלה הגיונית ואפשרית אבל האם הן מספקות? הרי אפשר להתעלל בעוד הרבה פקודות ב C אם לא עושים דברים נכון. זה כמו לאסור להשתמש במצביעים ל void הרי עם שימוש במצביעים כאלה אין type checking בחלק מהמקומות וזה יכול להוביל לשגיאות. זה פשוט לא הגיוני!

לעומת הטענות החלשות נגד goto יש טענות חזקות בעד:
1. אנשים משתמשים ב goto (בלי לדעת) כל הזמן.
2. goto עוזר מאוד בכל מיני מקרים.

סינטקס קיים שפועל כמו goto
1. break ו continue זה goto שעטפו אותו מעט! הרי בקלות אפשר לממש את שניהם בעזרת goto:

  while (true) {
  	if (i>1)
  		continue;
  	else
  		break;
  }

ועם goto:

  while (true) {
  next:
  	if (i>1)
  		goto next;
  	else
  		goto end;
  }
  end:

והמימוש עם לולאות for לא שונה בהרבה!

מה בנוגע ל try and catch? (כן, אני יודע שזה c++)

  try {
  	if (error) {
  		throw "error!";
  	}
  }
  catch (char * str) {
  	printf("%s\n", str);
  }

לעומת:

  if (error) {
  	str = "error!";
  }
  else {
  	goto cont;
  }
  
  /* catch */
  error:
  printf("%s\n", str);
  
  cont:

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

שימושים נכונים:

אני לדוגמא משתמש ב goto רק בשימוש אחד (אני חייב להודות שקיבלתי השראה מהקוד של הקרנל…) ניקוי פונקצייה אחרי שגיאה:
לדוגמא ללא שימוש ב goto:

  int
  foo (int i)
  {
  	char *a;
  	char *b;
  	char *c;
  
  	a = malloc(5);
  	if (a == null) 
  		return -1;
  
  	b = malloc(6);
  	if (b == null) {
  		free(a);
  		return -1;
  	}
  	
  	c = malloc(7);
  	if (c == null) {
  		free(a);
  		free(b);
  		return -1;
  	}
  	return 0;
  }

לעומת ניקיון בעזרת goto:

  int
  foo (int i)
  {
  	int ret=0;
  	char *a;
  	char *b;
  	char *c;
  
  	a = malloc(5);
  	if (a == null) 
  		goto errora;
  
  	b = malloc(6);
  	if (b == null) 
  		goto errorb;
  	
  	c = malloc(7);
  	if (c == null) 
  		goto errorc;
  
  exit:
  	return ret;
  
  
/* error handling section */  
  errorc:
  	free(b);
  errorb:
  	free(a);
  errora:
  	ret = -1;
  	goto exit;
  }

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

עוד דוגמא:

  int
  foo (int i)
  {
  	int stop = 0;
  	while (true) {
  		while (true) {
  			if (i==3) {
  				stop = 1;
  				break;
  			}
  		}
  		if (stop)
  			break;
  	}
  	/*more code */
  }
  

לעומת:

  int
  foo (int i)
  {
  	while (true) {
  		while (true) {
  			if (i==3) 
  				goto end;
  		}
  	}
  end:
  	/*more code */
  }

או עוד דוגמא:

  if (a) {
  	if (b) {
  		if (c) {
  			printf("a ");
  			printf("= b = c = true\n");
  		}
  	}
  	else {
  		if (d) {
  			printf("a = !b = d = true\n");
  		}
  	}
  }

לעומת:

  if (!a)
  	goto end;
  
  if (b) {
  	if (! c)
  		goto endc;
  
  	printf("a ");
  	printf("= b = c = true\n");
  	
  	endc:
  }
  else {
  	if (! d)
  		goto endd;
   	
  	printf("a = !b = d = true\n");
  	
  	endd:
  }
  
  end:

שהרבה יותר ברור (ויותר חשוב, מונע הזחות מיותרות!).

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

2. בלי קפיצות "אחורה".
לא מומלץ לכתוב קוד שבו הקפיצה תוביל "למעלה" במעלי הקוד, כלומר לקוד שהורץ לפני ה goto, לדוגמא, לא לעשות דבר כזה:

  start:
  /*code*/
  goto start;

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

4. יש לכם עוד רעיונות? תכתוב בתגובות…

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

:, ,
18 תגובות:
  1. Fate

    אני תמיד משתמש בgoto במקרה של נקיון דברים רבים שצריך לשחרר לפי הסדר..
    זיכרון, Handles, Sockets, וכו'..
    בלי זה זה פשוט Copy-Paste של הקוד שמנקה וזה פשוט נורא…

  2. TAsn

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

    אני משתמש ב goto בדיוק באותם מצבים שציינת, וזו הדרך הנכונה לעשות, copy paste זו פשוט שגיאה נוראית!

  3. spdr

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

  4. TAsn

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

  5. Exodus

    האמת שהתעייפתי באמצע הקריאה אבל סחטן על החפירה Tasn 😉
    חח
    בכל מקרה אני לא מוצא סיבה למה להחשיב את goto לדבר רע
    הרי ש-goto הוא מקביל מדוייק ל-jmp.
    והוא יכול להיות שימושי להרבה דברים שלא עולים לי לראש
    עכשיו חח.
    אבל חוץ מזה עזבו אותכם שטויות אני אומר עזבו מחשבים
    וחארטות בואו נעשן סמים ונהגר לדרום אמריקה או משהו

  6. TAsn

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

  7. Exodus

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

  8. TAsn

    טוב, אז אל תתחרט… 🙂
    ברור שבאסמבלי יהיה goto… ואכן זה גהנום לדבאג, אבל זה לא דוגמא לשימוש החכם והרגוע שאני מדבר עליו ב C 🙂

  9. spdr

    דרום אמריקה? פשש…תהנה! אני עוד מעט בא 🙂

  10. mvx

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

    בג'אווה כמו בג'אווה זה כמובן לא goto אמיתי אלא קישוט יפה (אך שימושי!) שמצאפשר לצאת מתנאי / לולאה לתווית מוגדרת. עוד בלינק הזה : http://geekycoder.wordpress.com/2008/06/25/tipjava-using-block-label-as-goto/

    אחלה בלוג, Fate\TAsn.
    הפכתי לקורא קבוע.

  11. gavra

    יש בזה משהו, אני אישית משתמש בו לפעמים (זה ממכר O: ) ואני לא מסכים עם הגישה של "אסור לקפוץ למעלה".
    לדוגמה:

    void RndToBoard()
    {
    int i, j;
    RAND_AGAIN:
    i = rand()%10;
    j = rand()%10;
    if (board[i][j] != EMPTY)
    goto RAND_AGAIN;
    board[i][j] = FULL;
    }

    מניח שהרעיון מובן, כמובן שאפשר לעשות זאת גם עם do while.

    בכל אופן, מתכנתים מנוסים רבים מתנגדים לשימוש ב goto, אני משתד ללכת בעקבותיהם (:

  12. TAsn

    gavra, אחי, הדוגמא שלך זה דוגמא קלאסית לשימוש שטני ואסור ב goto, למה לעזאזל שתרצה לעשות goto שאתה יכול לעשות את זה בלולאה בצורה יפה והגיונית?! (ונקייה), ככה שאתה לא מאבד את ההזחה.

    אז הדוגמא שלך היא ממש לא דוגמא נגדית לטענתי.

  13. working programmer

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

  14. TAsn

    working programmer, יש לך דוגמאות לאיך אתה היית עושה את זה? goto is considered harmful רק בגלל שטיפת מח מפגרת שאנשים עשו נגד זה בגלל אנשים שלא יודע לכתוב קוד. הדוגמאות שנתתי הן דוגמאות לקוד גרוע, ואחרי זה קוד עם goto ואני חושב ללא ספק שהקוד עם ה goto נקי יותר ונכון יותר.

    כמו שאתה בטח יודע PHP החדש קיבל תמיכה ב goto, למה אתה חושב שעשו את זה? כי goto זה רע?

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

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

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

    בנוסף, מה יש לך להגיד על ניקוי שגיאות בפונקציות?
    לאחרונה היה ב whatsup דיון ושם העלו כל מיני "פתרונות" אבל לאף אחד לא היה פתרון על איך לנקות פונקציות בצורה יפה בלי goto. או בקיצור, אני רוצה לשמוע מה יש לך לאמר, אבל תגיד אותו, אל תכתוב פה הערות חסרות תוכן 🙂

  15. gavra

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

  16. TAsn

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

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

  17. Kenny @ Maxercise

    הרבה אנשים נגד הרבה מאוד דברים טובים, רק בגלל שהם לא "מוכנים לזה"

השאר תגובה

מחפש משהו?

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