BinaryVision

Tag: goto

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

by on ינו.26, 2009, under כללי

מבוא
יש כל מיני "שיטות" תכנות, יש אנשים שמתכנתים "פרוצדוראלי", יש אנשים שמתכנתים "מונחה עצמים", יש אנשים שמעדיפים אבסטרקטיות ויש אנשים שלא. קיימות הרבה טענות בעד ונגד כל אחת מהשיטות, ישנה הסכמה שחלק מהשיטות נכונות יותר למקרים מסויימים, וחלק לאחרים, בקיצור, כולם מסכימים שאין שיטה אחת מושלמת, או לחלופין, נוראית. אולם, ישנה הסכמה ש 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 Comments :, , more...

מחפש משהו?

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