Руководство по стилю linux дает конкретные причины для использования goto
, которые соответствуют вашему примеру:
https://www.kernel.org/doc/Documentation/process/coding-style.rst
Обоснование использования gotos:
- безусловные заявления легче понять и следовать
- вложенность уменьшается
- ошибки, не обновляя отдельные точки выхода при внесении изменений, предотвращаются
- сохраняет работу компилятора для оптимизации избыточного кода;)
Отказ от ответственности Я не должен делиться своей работой. Приведенные здесь примеры немного надуманы, поэтому, пожалуйста, потерпите меня.
Это хорошо для управления памятью. Недавно я работал над кодом, в котором динамически выделялась память (например, char *
возвращаемая функцией). Функция, которая просматривает путь и выясняет, является ли путь допустимым, анализируя токены пути:
tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
...
some statements, some involving dynamically allocated memory
...
if ( check_this() ){
free(var1);
free(var2);
...
free(varN);
return 1;
}
...
some more stuff
...
if(something()){
if ( check_that() ){
free(var1);
free(var2);
...
free(varN);
return 1;
} else {
free(var1);
free(var2);
...
free(varN);
return 0;
}
}
token = strtok(NULL,delim);
}
free(var1);
free(var2);
...
free(varN);
return 1;
Теперь для меня следующий код намного приятнее и его легче поддерживать, если вам нужно добавить varNplus1
:
int retval = 1;
tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
...
some statements, some involving dynamically allocated memory
...
if ( check_this() ){
retval = 1;
goto out_free;
}
...
some more stuff
...
if(something()){
if ( check_that() ){
retval = 1;
goto out_free;
} else {
retval = 0;
goto out_free;
}
}
token = strtok(NULL,delim);
}
out_free:
free(var1);
free(var2);
...
free(varN);
return retval;
Теперь в коде были и другие проблемы, а именно то, что N было где-то выше 10, а функция была более 450 строк с 10 уровнями вложенности в некоторых местах.
Но я предложил своему супервайзеру провести его рефакторинг, что я и сделал, и теперь это набор функций, которые все короткие, и все они имеют стиль linux
int function(const char * param)
{
int retval = 1;
char * var1 = fcn_that_returns_dynamically_allocated_string(param);
if( var1 == NULL ){
retval = 0;
goto out;
}
if( isValid(var1) ){
retval = some_function(var1);
goto out_free;
}
if( isGood(var1) ){
retval = 0;
goto out_free;
}
out_free:
free(var1);
out:
return retval;
}
Если мы рассмотрим эквивалент без goto
s:
int function(const char * param)
{
int retval = 1;
char * var1 = fcn_that_returns_dynamically_allocated_string(param);
if( var1 != NULL ){
if( isValid(var1) ){
retval = some_function(var1);
} else {
if( isGood(var1) ){
retval = 0;
}
}
free(var1);
} else {
retval = 0;
}
return retval;
}
Для меня, в первом случае, для меня очевидно, что если первая функция вернется NULL
, мы уйдем отсюда и вернемся 0
. Во втором случае мне нужно прокрутить вниз, чтобы увидеть, что if содержит всю функцию. Конечно, первый указывает на это для меня стилистически (название « out
»), а второй - на синтаксический. Первый еще более очевиден.
Кроме того, я предпочитаю иметь free()
операторы в конце функции. Это отчасти потому, что, по моему опыту, free()
операторы в середине функций плохо пахнут и указывают мне, что я должен создать подпрограмму. В этом случае я создал var1
свою функцию и не смог free()
ее выполнить в подпрограмме, но поэтому goto out_free
стиль goto out настолько практичен.
Я думаю, что программистов нужно воспитывать, полагая, что goto
это зло. Затем, когда они станут достаточно зрелыми, им следует просмотреть исходный код Linux и прочитать руководство по стилю linux.
Я должен добавить, что я использую этот стиль очень последовательно, каждая функция имеет int retval
, out_free
label и out label. Благодаря стилистической последовательности улучшается читаемость.
Бонус: ломается и продолжается
Скажем, у вас есть время цикла
char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
var1 = functionA(line,count);
var2 = functionB(line,count);
if( functionC(var1, var2){
count++
continue;
}
...
a bunch of statements
...
count++;
free(var1);
free(var2);
}
Есть другие вещи не так с этим кодом, но одна вещь, это оператор continue. Я хотел бы переписать все это, но мне было поручено немного изменить его. Мне потребовалось бы несколько дней, чтобы провести рефакторинг таким образом, чтобы это меня удовлетворило, но фактическое изменение заняло около половины дня. Проблема в том, что даже если мы « continue
», нам все равно нужно освободить var1
и var2
. Я должен был добавить var3
, и это заставило меня хотеть рвать, чтобы отражать выражения free ().
Я был относительно новым стажером в то время, но некоторое время назад я искал исходный код linux для развлечения, поэтому я спросил своего руководителя, могу ли я использовать оператор goto. Он сказал да, и я сделал это:
char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
var1 = functionA(line,count);
var2 = functionB(line,count);
var3 = newFunction(line,count);
if( functionC(var1, var2){
goto next;
}
...
a bunch of statements
...
next:
count++;
free(var1);
free(var2);
}
Я думаю, что продолжение в порядке в лучшем случае, но для меня это как goto с невидимым лейблом. То же самое касается перерывов. Я бы все же предпочел продолжить или прервать, если, как это было в данном случае, это не заставит вас отражать изменения в нескольких местах.
И я должен также добавить, что это использование goto next;
и next:
этикетка являются неудовлетворительными для меня. Они просто лучше, чем зеркальное отражение free()
и count++
утверждения.
goto
Почти всегда они ошибаются, но нужно знать, когда они полезны.
Одна вещь, которую я не обсуждал, это обработка ошибок, которая была покрыта другими ответами.
Представление
Можно посмотреть на реализацию strtok () http://opensource.apple.com//source/Libc/Libc-167/string.subproj/strtok.c
#include <stddef.h>
#include <string.h>
char *
strtok(s, delim)
register char *s;
register const char *delim;
{
register char *spanp;
register int c, sc;
char *tok;
static char *last;
if (s == NULL && (s = last) == NULL)
return (NULL);
/*
* Skip (span) leading delimiters (s += strspn(s, delim), sort of).
*/
cont:
c = *s++;
for (spanp = (char *)delim; (sc = *spanp++) != 0;) {
if (c == sc)
goto cont;
}
if (c == 0) { /* no non-delimiter characters */
last = NULL;
return (NULL);
}
tok = s - 1;
/*
* Scan token (scan for delimiters: s += strcspn(s, delim), sort of).
* Note that delim must have one NUL; we stop if we see that, too.
*/
for (;;) {
c = *s++;
spanp = (char *)delim;
do {
if ((sc = *spanp++) == c) {
if (c == 0)
s = NULL;
else
s[-1] = 0;
last = s;
return (tok);
}
} while (sc != 0);
}
/* NOTREACHED */
}
Пожалуйста, исправьте меня, если я ошибаюсь, но я верю, что cont:
метка и goto cont;
утверждение предназначены для повышения производительности (они, конечно, не делают код более читабельным). Их можно заменить читабельным кодом, выполнив
while( isDelim(*s++,delim));
пропустить разделители. Но чтобы быть максимально быстрым и избежать ненужных вызовов функций, они делают это таким образом.
Я прочитал статью Дейкстры и нахожу ее довольно эзотерической.
Google "Dijkstra Goto заявление считается вредным", потому что у меня недостаточно репутации, чтобы разместить более 2 ссылок.
Я видел, что это цитируется как причина не использовать goto, и чтение ничего не изменило, если принять во внимание, что я использую goto.
Приложение :
Я придумал аккуратное правило, думая обо всем этом о продолжениях и перерывах.
- Если в цикле while у вас есть continue, тогда тело цикла while должно быть функцией, а continue - оператором return.
- Если в цикле while у вас есть оператор break, то сам цикл while должен быть функцией, а разрыв должен стать оператором return.
- Если у вас есть оба, то что-то может быть не так.
Это не всегда возможно из-за проблем с областью видимости, но я обнаружил, что это значительно упрощает анализ моего кода. Я заметил, что всякий раз, когда в цикле while происходит разрыв или продолжение, это вызывает у меня плохое предчувствие.