Выгодно ли когда-либо использовать «goto» в языке, который поддерживает циклы и функции? Если так, то почему? - PullRequest
189 голосов
/ 23 августа 2008

У меня давно сложилось впечатление, что goto никогда не следует использовать, если это возможно. Просматривая libavcodec (который написан на C) на днях, я заметил многократное его использование. Всегда ли выгодно использовать goto в языке, который поддерживает циклы и функции? Если так, то почему?

Ответы [ 24 ]

893 голосов
/ 11 мая 2010

Каждый, кто против - goto, прямо или косвенно цитирует статью Эдсгера Дейкстры GoTo, которая считается вредной , чтобы обосновать свою позицию. Жаль, что статья Дейкстры практически не имеет ничего общего с тем, как в наши дни используются операторы * 1006 и, следовательно, то, что говорится в статье, практически не применимо к современной сцене программирования. Безмолвный мем теперь граничит с религией, вплоть до ее священных писаний, диктуемых свыше, его первосвященников и избегания (или еще хуже) воспринимаемых еретиков.

Давайте поместим статью Дейкстры в контекст, чтобы пролить немного света на эту тему.

Когда Дейкстра написал свою статью, популярные языки того времени были неструктурированными процедурными, такими как бейсик, фортран (более ранние диалекты) и различные языки ассемблера. Люди, использующие языки более высокого уровня, довольно часто перепрыгивали по всей своей кодовой базе в искаженных, искаженных потоках выполнения, что породило термин «код спагетти». Вы можете увидеть это, перейдя к классической игре Trek , написанной Майком Мэйфилдом и попытавшись выяснить, как все работает. Потратьте несколько минут, чтобы просмотреть это.

НАСТОЯЩЕЕ - это «необузданное использование утверждения« идти к »», против которого Дейкстра ругался в своей газете в 1968 году. эта бумага. Возможность прыгать в любом месте вашего кода в любое удобное для вас место было тем, что он критиковал и требовал, чтобы его остановили. Сравнивать это с анемичными способностями goto в C или других подобных более современных языках просто невозможно.

Я уже слышу возвышенные крики культистов, когда они сталкиваются с еретиком. «Но, - будут повторять они, - вы можете сделать код очень трудным для чтения с goto в C.» О да? Вы также можете сделать код очень трудным для чтения без goto. Как этот:

#define _ -F<00||--F-OO--;
int F=00,OO=00;main(){F_OO();printf("%1.3f\n",4.*-F/OO/OO);}F_OO()
{
            _-_-_-_
       _-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
        _-_-_-_-_-_-_-_
            _-_-_-_
}

Не goto в поле зрения, поэтому его должно быть легко читать, верно? Или как насчет этого:

a[900];     b;c;d=1     ;e=1;f;     g;h;O;      main(k,
l)char*     *l;{g=      atoi(*      ++l);       for(k=
0;k*k<      g;b=k       ++>>1)      ;for(h=     0;h*h<=
g;++h);     --h;c=(     (h+=g>h     *(h+1))     -1)>>1;
while(d     <=g){       ++O;for     (f=0;f<     O&&d<=g
;++f)a[     b<<5|c]     =d++,b+=    e;for(      f=0;f<O
&&d<=g;     ++f)a[b     <<5|c]=     d++,c+=     e;e= -e
;}for(c     =0;c<h;     ++c){       for(b=0     ;b<k;++
b){if(b     <k/2)a[     b<<5|c]     ^=a[(k      -(b+1))
<<5|c]^=    a[b<<5      |c]^=a[     (k-(b+1     ))<<5|c]
;printf(    a[b<<5|c    ]?"%-4d"    :"    "     ,a[b<<5
|c]);}      putchar(    '\n');}}    /*Mike      Laman*/

Нет goto там тоже. Поэтому он должен быть читаемым.

Какой смысл в этих примерах? Это не языковые функции, которые делают нечитаемый, не поддерживаемый код. Это не синтаксис, который делает это. Это плохие программисты, которые вызывают это. И плохие программисты, как вы можете видеть в этом пункте, могут сделать любую языковую функцию нечитаемой и непригодной для использования. Как for петли там. (Вы можете их видеть, верно?)

Теперь, чтобы быть справедливым, некоторые языковые конструкции легче злоупотреблять, чем другие. Однако, если вы программист на C, я бы посмотрел гораздо ближе примерно на 50% случаев использования #define задолго до того, как отправился бы в поход против goto!

Итак, для тех, кто потрудился прочитать это далеко, есть несколько ключевых моментов, на которые следует обратить внимание.

  1. Статья Дейкстры о goto заявлениях была написана для среды программирования, где goto был лот более опасно, чем в большинстве современных языков, которые не являются ассемблером.
  2. Автоматически отбрасывать все случаи использования goto, потому что это примерно так же рационально, как сказать: «Я пытался когда-то веселились, но мне это не нравилось, так что теперь я против ».
  3. Существует законное использование современных (анемичных) goto операторов в коде, которые не могут быть адекватно заменены другими конструкциями.
  4. Есть, конечно, незаконное использование тех же утверждений.
  5. Существует также незаконное использование современных управляющих операторов, таких как мерзость "godo", где всегда ложная петля do не используется break вместо goto. Они часто хуже, чем разумное использование goto.
228 голосов
/ 23 августа 2008

Есть несколько причин для использования оператора "goto", о котором я знаю (некоторые уже говорили об этом):

Чистый выход из функции

Часто в функции вы можете распределять ресурсы и должны выходить из нескольких мест. Программисты могут упростить свой код, поместив код очистки ресурса в конец функции, и все «точки выхода» функции перейдут к метке очистки. Таким образом, вам не нужно писать код очистки в каждой «точке выхода» функции.

Выход из вложенных циклов

Если вы находитесь во вложенном цикле и хотите выйти из всех циклов , goto может сделать это намного чище и проще, чем операторы break и if-проверки.

Улучшения производительности на низком уровне

Это действительно только в критически важном коде, но операторы goto выполняются очень быстро и могут дать вам импульс при перемещении по функции. Однако это обоюдоострый меч, потому что компилятор обычно не может оптимизировать код, содержащий gotos.

Обратите внимание, что во всех этих примерах gotos ограничены областью действия одной функции.

147 голосов
/ 02 февраля 2010

Слушание лучших практик вслепую не является лучшей практикой. Идея избегать операторов goto как основной формы управления потоком состоит в том, чтобы избежать создания нечитаемого кода спагетти. Если их экономно использовать в нужных местах, иногда они могут быть самым простым и ясным способом выражения идеи. Уолтер Брайт, создатель компилятора Zortech C ++ и языка программирования D, использует их часто, но разумно. Даже с операторами goto его код по-прежнему отлично читается.

Итог: избегать goto ради избегания goto бессмысленно. Чего вы действительно хотите избежать, так это создавать нечитаемый код. Если ваш goto -груженный код читабелен, то в этом нет ничего плохого.

38 голосов
/ 23 августа 2008

Поскольку goto затрудняет рассуждение о ходе выполнения программы 1 (он же «код спагетти»), goto обычно используется только для компенсации отсутствующих функций: использование goto может фактически быть приемлемым, но только если язык не предлагает более структурированный вариант для достижения той же цели. Возьмите пример сомнения:

Правило для goto, которое мы используем, заключается в том, что goto подходит для перехода вперед к единственной точке очистки выхода в функции.

Это верно - но только если язык не позволяет структурированную обработку исключений с кодом очистки (таким как RAII или finally), который выполняет ту же работу лучше (так как он специально создан для этого), или когда есть веская причина не использовать структурированную обработку исключений (но у вас никогда не будет этого случая, кроме как на очень низком уровне).

В большинстве других языков единственное допустимое использование goto - выход из вложенных циклов. И даже там почти всегда лучше поднять внешний цикл в собственный метод и использовать вместо него return.

Кроме этого, goto является признаком того, что недостаточно внимания было вложено в конкретный фрагмент кода.


1 В современных языках, поддерживающих goto, реализованы некоторые ограничения (например, goto может не входить и не выходить из функций), но проблема в основном остается той же.

Кстати, то же самое, разумеется, справедливо и для других языковых функций, в частности, для исключений. И, как правило, существуют строгие правила использования этих функций только там, где это указано, например, правило не использовать исключения для управления неисключительным потоком программ.

34 голосов
/ 11 мая 2010

Ну, есть одна вещь, которая всегда хуже, чем goto's; странное использование других операторов потока программ, чтобы избежать перехода:

Примеры:

    // 1
    try{
      ...
      throw NoErrorException;
      ...
    } catch (const NoErrorException& noe){
      // This is the worst
    } 


    // 2
    do {
      ...break; 
      ...break;
    } while (false);


    // 3
    for(int i = 0;...) { 
      bool restartOuter = false;
      for (int j = 0;...) {
        if (...)
          restartOuter = true;
      if (restartOuter) {
        i = -1;
      }
    }

etc
etc
27 голосов
/ 23 августа 2008

In C # switch оператор не допускает провала . Таким образом, goto используется для передачи управления определенной метке переключения или метке по умолчанию .

Например:

switch(value)
{
  case 0:
    Console.Writeln("In case 0");
    goto case 1;
  case 1:
    Console.Writeln("In case 1");
    goto case 2;
  case 2:
    Console.Writeln("In case 2");
    goto default;
  default:
    Console.Writeln("In default");
    break;
}

Редактировать: есть одно исключение из правила "без провала". Прорыв разрешен, если в регистре нет кода.

14 голосов
/ 25 августа 2008

#ifdef TONGUE_IN_CHEEK

Perl имеет goto, который позволяет вам реализовывать хвостовые вызовы бедняков. : -Р

sub factorial {
    my ($n, $acc) = (@_, 1);
    return $acc if $n < 1;
    @_ = ($n - 1, $acc * $n);
    goto &factorial;
}

#endif

Хорошо, так что это не имеет ничего общего с C goto. Более серьезно, я согласен с другими комментариями об использовании goto для очистки, или для реализации устройства Даффа , или тому подобное. Все дело в использовании, а не в злоупотреблении.

(Тот же комментарий может применяться к longjmp, исключениям, call/cc и т. Д. - они имеют законное использование, но могут легко использоваться неправильно. Например, чисто исключительное исключение избежать глубоко вложенной структуры управления при совершенно неисключительных обстоятельствах.)

12 голосов
/ 16 сентября 2008

Я написал более нескольких строк на ассемблере за эти годы. В конечном счете, каждый язык высокого уровня компилируется в gotos. Хорошо, назовите их "ветви" или "прыжки" или как-нибудь еще, но они gotos. Кто-нибудь может написать goto-less ассемблер?

Теперь, конечно, вы можете указать программисту на Fortran, C или BASIC, что запуск бунта с gotos - это рецепт для болгарского спагетти. Ответ, однако, заключается не в том, чтобы избежать их, а в том, чтобы использовать их осторожно.

Нож можно использовать для приготовления пищи, освобождения кого-либо или убийства. Обойдемся ли мы без ножей из-за страха перед последними? Точно так же goto: используется небрежно, мешает, используется осторожно, помогает.

8 голосов
/ 26 января 2014

Посмотрите на Когда использовать Goto при программировании на C :

Хотя использование goto - почти всегда плохая практика программирования (конечно, вы можете найти лучший способ сделать XYZ), бывают случаи, когда это действительно не плохой выбор. Некоторые могут даже утверждать, что, когда это полезно, это лучший выбор.

Большая часть того, что я должен сказать о goto, действительно относится только к C. Если вы используете C ++, нет веских оснований использовать goto вместо исключений. В C, однако, у вас нет возможностей механизма обработки исключений, поэтому, если вы хотите отделить обработку ошибок от остальной логики вашей программы и хотите избежать многократного переписывания кода очистки во всем коде, тогда Goto может быть хорошим выбором.

Что я имею в виду? Вы могли бы иметь некоторый код, который выглядит так:

int big_function()
{
    /* do some work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* clean up*/
    return [success];
}

Это нормально, пока вы не поймете, что вам нужно изменить код очистки. Затем вы должны пройти и внести 4 изменения. Теперь вы можете решить, что можете просто инкапсулировать всю очистку в одну функцию; это не плохая идея. Но это означает, что вам нужно быть осторожным с указателями - если вы планируете освободить указатель в своей функции очистки, нет способа установить для него указатель на NULL, если вы не передадите указатель на указатель. Во многих случаях вы не будете использовать этот указатель в любом случае, так что это не может быть серьезной проблемой. С другой стороны, если вы добавите новый указатель, дескриптор файла или другую вещь, которая нуждается в очистке, то вам нужно будет снова изменить свою функцию очистки; и тогда вам нужно будет изменить аргументы этой функции.

При использовании goto это будет

int big_function()
{
    int ret_val = [success];
    /* do some work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
end:
    /* clean up*/
    return ret_val;
}

Преимущество здесь в том, что ваш код, следующий за концом, имеет доступ ко всему, что ему потребуется для очистки, и вам удалось значительно сократить количество точек изменения. Другое преимущество состоит в том, что вы перешли от нескольких точек выхода для вашей функции к одной; нет никакого шанса, что вы случайно вернетесь из функции без очистки.

Более того, поскольку goto используется только для перехода к одной точке, это не значит, что вы создаете массу спагетти-кода, перемещающегося взад и вперед в попытке симулировать вызовы функций. Скорее, goto на самом деле помогает писать более структурированный код.


Одним словом, goto всегда следует использовать экономно и в крайнем случае - но для этого есть время и место. Вопрос должен быть не в том, «нужно ли вам это использовать», а в том, «лучший ли это выбор», чтобы его использовать.

7 голосов
/ 11 мая 2010

Мне показалось забавным, что некоторые люди заходят так далеко, что приводят список случаев, когда goto приемлемо, говоря, что любое другое использование недопустимо. Вы действительно думаете, что знаете каждый случай, когда goto - лучший выбор для выражения алгоритма?

Для иллюстрации приведу пример, который еще никто здесь не показывал:

Сегодня я писал код для вставки элемента в хеш-таблицу. Хеш-таблица представляет собой кэш предыдущих вычислений, который может быть перезаписан по желанию (влияет на производительность, но не на корректность).

В каждом сегменте хеш-таблицы есть 4 слота, и у меня есть несколько критериев, чтобы решить, какой элемент перезаписать при заполнении сегмента. Прямо сейчас это означает сделать до трех проходов через ковш, как это:

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    goto add;

// Otherwise, find first empty element
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
    goto add;

// Additional passes go here...

add:
// element is written to the hash table here

Теперь, если бы я не использовал goto, как бы выглядел этот код?

Примерно так:

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    break;

if (add_index >= ELEMENTS_PER_BUCKET) {
  // Otherwise, find first empty element
  for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
    if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
      break;
  if (add_index >= ELEMENTS_PER_BUCKET)
   // Additional passes go here (nested further)...
}

// element is written to the hash table here

Это будет выглядеть хуже и хуже, если будет добавлено больше проходов, в то время как версия с goto всегда сохраняет один и тот же уровень отступа и избегает использования ложных операторов if, результат которых подразумевается при выполнении предыдущего цикла.

Так что есть еще один случай, когда goto делает код чище и легче писать и понимать ... Я уверен, что есть еще много, так что не притворяйтесь, что вы знаете все случаи, где goto полезен, отбрасывая любые хорошие что вы не могли придумать.

...