К каким ошибкам приводят утверждения «goto»? Есть ли исторически значимые примеры?

103

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

XKCD Alt Text: «Нил Стивенсон считает, что называть его ярлыки« деньгами »мило».
См. Оригинальный комикс по адресу: http://xkcd.com/292/

Потому что я узнал это рано; У меня нет никакого понимания или опыта, к каким типам ошибок gotoэто приводит. Итак, о чем мы говорим здесь:

  • Нестабильность?
  • Неработоспособный или нечитаемый код?
  • Уязвимости безопасности?
  • Что-то еще целиком?

К каким ошибкам в действительности приводят утверждения "goto"? Есть ли исторически значимые примеры?

Акива
источник
33
Вы читали оригинальную короткую статью Дейкстры на эту тему? (Это вопрос «да / нет», и любой ответ, кроме однозначного мгновенного ответа «да», означает «нет».)
Джон Р. Штром,
2
Я предполагаю, что тревожный выход - это когда случается что-то катастрофическое. откуда вы никогда не вернетесь. Как сбой питания, исчезновение устройств или файлов. Некоторые виды "по ошибке" ... Но я думаю, что большинство "почти катастрофических" ошибок в настоящее время могут быть обработаны конструкциями "try - catch".
Ленн
13
Никто еще не упомянул рассчитанный GOTO. Еще тогда, когда Земля была молодой, Бейсик имел номера строк. Вы можете написать следующее: 100 N = A * 100; GOTO N. Попробуйте отладить это :)
mcottle
7
@ JohnR.Strohm Я прочитал статью Дейкстры. Он был написан в 1968 году и предполагает, что потребность в gotos может быть уменьшена в языках, которые включают в себя такие расширенные функции, как операторы и функции switch. Он был написан в ответ на языки, на которых goto был основным методом управления потоком, и его можно было использовать для перехода к любой точке программы. Принимая во внимание, что в языках, которые были разработаны после написания этой статьи, например, C, goto может переходить только в места в том же фрейме стека и обычно используется только тогда, когда другие опции не работают. (продолжение ...)
Рэй
10
(... продолжение) Его пункты были действительны в то время, но они больше не актуальны, независимо от того, является ли goto хорошей идеей или нет. Если мы хотим утверждать, что так или иначе, должны быть приведены аргументы, относящиеся к современным языкам. Кстати, вы читали "Структурное программирование с помощью операторов goto" Кнута?
Луч

Ответы:

2

Вот способ посмотреть на это, я еще не видел в других ответах.

Речь идет о сфере. Один из главных столпов хорошей практики программирования - держать ваш прицел в узком месте. Вам нужен узкий кругозор, потому что у вас нет умственных способностей контролировать и понимать больше, чем просто несколько логических шагов. Таким образом, вы создаете маленькие блоки (каждый из которых становится «одной вещью»), а затем строите их, чтобы создать большие блоки, которые становятся одной вещью, и так далее. Это делает вещи управляемыми и понятными.

Goto эффективно раздувает объем вашей логики для всей программы. Это наверняка победит ваш мозг для любых, кроме самых маленьких программ, занимающих всего несколько строк.

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

Мартин Маат
источник
Нелокальный goto?
дедупликатор
thebrain.mcgill.ca/flash/capsules/experience_jaune03.html << Человеческий разум может отслеживать в среднем 7 вещей одновременно. Ваше обоснование играет на этом ограничении, поскольку расширение сферы действия добавит еще много вещей, которые необходимо отслеживать. Таким образом, типы ошибок, которые будут внесены, скорее всего, являются ошибкой и забывчивостью. Вы также предлагаете стратегию смягчения последствий и хорошую практику в целом, заключающуюся в том, чтобы «держать свой прицел в напряжении». Таким образом, я принимаю этот ответ. Хорошая работа, Мартин.
Акива
1
Как это круто?! Среди более 200 отданных голосов, выиграв всего 1!
Мартин Маат
68

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

При программировании блок-схем (которые изучали люди моего поколения и которые использовали для программы Apollo moon) вы создаете диаграмму с блоками для выполнения операторов и бриллиантами для принятия решений, и вы можете соединить их линиями, которые идут повсюду. (Так называемый «код спагетти».)

Проблема с кодом спагетти в том, что вы, программист, могли «знать», что это правильно, но как вы можете доказать это себе или кому-либо еще? На самом деле, это может иметь потенциальное неправильное поведение, и ваше знание того, что оно всегда правильно, может быть ошибочным.

Наряду с этим появилось структурированное программирование с начальными и конечными блоками, пока, если-еще и так далее. У них было преимущество в том, что вы все равно могли делать с ними что угодно, но если вы были осторожны, вы могли быть уверены, что ваш код был верным.

Конечно, люди все еще могут писать спагетти-код даже без него goto. Распространенным методом является запись a while(...) switch( iState ){..., где в разных случаях устанавливаются iStateразные значения. На самом деле, в C или C ++ можно написать макрос, чтобы сделать это, и назвать его GOTO, так что сказать, что вы не используете, goto- это различие без разницы.

В качестве примера того, как проверка кода может предотвратить неограниченное goto, я давным-давно наткнулся на структуру управления, полезную для динамически изменяющихся пользовательских интерфейсов. Я назвал это дифференциальным исполнением . Это полностью Тьюринга универсальное, но его доказательство корректности зависит от чисто структурного программирования - нет goto, return, continue, break, или исключение.

введите описание изображения здесь

Майк Данлавей
источник
49
Корректность не может быть доказана, за исключением самых тривиальных программ, даже когда для их написания используется структурированное программирование. Вы можете только повысить свой уровень доверия; Структурное программирование делает это.
Роберт Харви
23
@MikeDunlavey: Связано: «Остерегайтесь ошибок в приведенном выше коде; я только доказал, что это правильно, а не пробовал». staff.fnwi.uva.nl/p.vanemdeboas/knuthnote.pdf
Утка для
26
@RobertHarvey Не совсем верно, что доказательства корректности применимы только для тривиальных программ. Однако необходимы более специализированные инструменты, чем структурированное программирование.
Майк Хаскель
18
@MSalters: это исследовательский проект. Цель такого исследовательского проекта - показать, что они могли бы написать проверенный компилятор, если бы у них были ресурсы. Если вы посмотрите на их текущую работу, вы увидите, что они просто не заинтересованы в поддержке более поздних версий C, а заинтересованы в расширении свойств корректности, которые они могут доказать. И для этого просто не имеет значения, поддерживают ли они C90, C99, C11, Pascal, Fortran, Algol или Plankalkül.
Йорг W Mittag
8
@martineau: Я с подозрением отношусь к этим «бозонным фразам», когда программисты вроде как соглашаются, что это плохо или хорошо, потому что они сядут, если не сделают этого. Там должно быть больше основных причин для вещей.
Майк Данлавей,
63

Почему Goto опасно?

  • gotoне вызывает нестабильности сам по себе. Несмотря на примерно 100 000 gotoс, ядро ​​Linux остается моделью стабильности.
  • gotoсамо по себе не должно вызывать уязвимостей в безопасности. Однако в некоторых языках смешивание его с блоками управления try/ catchисключений может привести к уязвимости, как объяснено в этой рекомендации CERT . Основные компиляторы C ++ помечают и предотвращают такие ошибки, но, к сожалению, старые или более экзотические компиляторы этого не делают.
  • gotoвызывает нечитаемый и не поддерживаемый код. Это также называется кодом спагетти , потому что, как и в тарелке спагетти, очень трудно следить за потоком контроля, когда слишком много gotos.

Даже если вам удастся избежать спагетти-кода и если вы используете только несколько gotos, они все равно способствуют таким ошибкам, как утечка ресурсов:

  • Код, использующий структурное программирование с четкими вложенными блоками и циклами или переключателями, прост для понимания; поток управления очень предсказуем. Поэтому проще обеспечить соблюдение инвариантов.
  • С помощью gotoзаявления вы нарушаете этот простой поток и разбиваете ожидания. Например, вы можете не заметить, что вам еще нужно освободить ресурсы.
  • Многие gotoв разных местах могут отправить вас к одной цели goto. Так что не очевидно, чтобы точно знать, в каком состоянии вы находитесь, достигнув этого места. Следовательно, риск ошибочных / необоснованных предположений достаточно велик.

Дополнительная информация и цитаты:

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

  • Джеймс Гослинг и Генри МакГилтон написали в своем техническом документе по языковой среде Java 1995 года :

    Нет больше
    операторов Goto У Java нет оператора Goto. Исследования показали, что goto (неправильно) используется чаще, чем просто «потому что оно есть». Исключение goto привело к упрощению языка (...) Изучение примерно 100 000 строк кода на C показало, что примерно 90 процентов операторов goto были использованы исключительно для получения эффекта разрыва вложенных циклов. Как уже упоминалось выше, многоуровневый разрыв и продолжение устраняют большую часть необходимости в операторах goto.

  • Бьярн Страуструп определяет Гото в своем глоссарии в таких привлекательных терминах:

    Гото - позорное Гото. В первую очередь полезно для машинно-сгенерированного кода C ++.

Когда можно пойти?

Как и K & R, я не догматичен по поводу gotos. Я признаю, что бывают ситуации, когда goto может облегчить жизнь.

Как правило, в C goto разрешает многоуровневый выход из цикла или обработку ошибок, требующую достижения соответствующей точки выхода, которая освобождает / разблокирует все ресурсы, которые были выделены до сих пор (т.е. многократное распределение в последовательности означает несколько меток). Эта статья дает количественную оценку различных вариантов использования goto в ядре Linux.

Лично я предпочитаю избегать этого, и через 10 лет я использовал максимум 10 gotos. Я предпочитаю использовать вложенные ifs, которые я считаю более читабельными. Когда это приведет к слишком глубокому вложению, я решу либо разложить свою функцию на более мелкие части, либо использовать логический индикатор в каскаде. Современные оптимизирующие компиляторы достаточно умны, чтобы генерировать практически тот же код, что и тот же код, с которым goto.

Использование goto сильно зависит от языка:

  • В C ++ правильное использование RAII заставляет компилятор автоматически уничтожать объекты, которые выходят за пределы области видимости, так что ресурсы / блокировки все равно будут очищены, и больше нет необходимости в goto.

  • В Java нет необходимости Гото (см автора цитаты в Java выше , и этот превосходный Stack Overflow ответ ): сборщик мусора , который очищает беспорядок, break, continueи try/ catchобработка исключений охватывает все тот случай , когда gotoможет быть полезными, но в более безопасном и лучше манера. Популярность Java доказывает, что на современном языке можно избежать утверждения goto.

Увеличьте известную уязвимость SSL goto fail

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

Я не знаю, сколько серьезных ошибок связано с gotoисторией программирования: детали часто не сообщаются. Однако была известная ошибка Apple SSL, которая ослабила безопасность iOS. Утверждение, которое привело к этой ошибке, было неверным goto.

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

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

Ошибка в функции SSLEncodeSignedServerKeyExchange()в этом файле :

    if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0)
        goto fail;
    if ((err =...) !=0)
        goto fail;
    if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
        goto fail;
        goto fail;  // <====OUCH: INDENTATION MISLEADS: THIS IS UNCONDITIONDAL!!
    if (...)
        goto fail;

    ... // Do some cryptographic operations here

fail:
    ... // Free resources to process error

Действительно, фигурные скобки вокруг условного блока могли бы предотвратить ошибку:
это привело бы либо к синтаксической ошибке при компиляции (и, следовательно, к исправлению), либо к избыточному безопасному переходу. Кстати, GCC 6 сможет обнаружить эти ошибки благодаря своему необязательному предупреждению для обнаружения несовместимых отступов.

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

Подход 1: если оговорка или вложенная ifs

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

    if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0 &&
        (err = ...) == 0 ) &&
        (err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) &&
        ...
        (err = ...) == 0 ) )
    {
         ... // Do some cryptographic operations here
    }
    ... // Free resources

Подход 2: использовать аккумулятор ошибок

Этот подход основан на том факте, что почти все приведенные здесь операторы вызывают некоторую функцию для установки errкода ошибки и выполняют остальную часть кода только в том случае, если errбыло 0 (то есть функция выполнялась без ошибок). Хорошая безопасная и читаемая альтернатива:

bool ok = true;
ok =  ok && (err = ReadyHash(&SSLHashSHA1, &hashCtx))) == 0;
ok =  ok && (err = NextFunction(...)) == 0;
...
ok =  ok && (err = ...) == 0;
... // Free resources

Здесь нет ни одного перехода: нет риска быстро перейти к точке выхода из строя. И визуально было бы легко обнаружить смещенную линию или забытую ok &&.

Эта конструкция более компактна. Он основан на том факте, что в C вторая часть логического и ( &&) оценивается, только если первая часть истинна. Фактически, ассемблер, сгенерированный оптимизирующим компилятором, почти эквивалентен исходному коду с gotos: оптимизатор очень хорошо обнаруживает цепочку условий и генерирует код, который при первом ненулевом возвращаемом значении переходит к концу ( онлайн-подтверждение ).

Вы могли бы даже предусмотреть проверку согласованности в конце функции, которая могла бы на этапе тестирования выявлять несоответствия между флагом ok и кодом ошибки.

assert( (ok==false && err!=0) || (ok==true && err==0) );

Ошибки, такие как ==0непреднамеренная замена !=0логическим или ошибочным соединителем, легко обнаружатся на этапе отладки.

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

Christophe
источник
34
Нет, ошибка Apple goto fail была вызвана умным программистом, который решил не включать фигурные скобки после оператора if. :-) Поэтому, когда появился следующий программист обслуживания (или тот же самый, с той же разницей) и добавил еще одну строку кода, которая должна была выполняться только тогда, когда оператор if оценивал значение true, оператор выполнялся каждый раз. Бэм, ошибка «goto fail», которая была серьезной ошибкой безопасности. Но это не имело никакого отношения к тому факту, что использовалось выражение goto.
Крейг,
47
Ошибка Apple «goto fail» была вызвана дублированием строки кода. Особый эффект этой ошибки был вызван тем, что эта строка была gotoвнутри оператора без скобок if. Но если вы предполагаете, что избегание gotoсделает ваш код невосприимчивым к эффекту случайно дублированных строк, у меня для вас плохие новости.
user253751
12
Вы используете сокращенное логическое поведение оператора для выполнения оператора через побочный эффект и утверждаете, что это более понятно, чем ifутверждение? Веселые времена. :)
Крейг
38
Если бы вместо «goto fail» было выражение «fail = true», то произошло бы то же самое.
gnasher729
15
Эта ошибка была вызвана неаккуратным разработчиком, который не обращал внимания на то, что он или она делал, и не читал собственные изменения кода. Ни больше ни меньше. Ладно, может быть, добавим несколько пробных пропусков.
Гонки легкости на орбите
34

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

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

В рамках этой функции вы должны иметь возможность делать все, что пожелаете, в пределах разумного. Если использование функции goto или two делает ее более понятной или повышает вашу скорость, почему бы и нет? Смысл функции в том, чтобы отделить немного четко определенной функциональности, чтобы вам больше не приходилось думать о том, как она работает внутри. Как только это написано, вы просто используете его.

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

введите описание изображения здесь

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

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

Роберт Харви
источник
2
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
maple_shaft
11

К каким ошибкам приводят утверждения «goto»? Есть ли исторически значимые примеры?

Раньше я использовал gotoоператоры при написании программ BASIC в детстве, как простой способ получить циклы for и while (в Commodore 64 BASIC не было циклов while, и я был слишком незрелым, чтобы изучать правильный синтаксис и использование циклов for. ). Мой код был часто тривиальным, но любые ошибки цикла могли быть немедленно приписаны моему использованию goto.

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

Когда в 1968 году Эдсгер Дейкстра объявил «Goto признан вредным», он не привел несколько примеров, в которых можно было бы обвинить связанные ошибки в Goto, скорее, он заявил, что это gotoне нужно для языков более высокого уровня и что его следует избегать в пользу того, что сегодня мы рассматриваем нормальный поток управления: циклы и условные выражения. Его слова:

Необузданное использование команды go to немедленно приводит к тому, что становится очень трудно найти значимый набор координат, в которых можно описать ход процесса.
[...] перейти к заявлению , поскольку это стоит только слишком примитивно; это слишком большое приглашение, чтобы испортить свою программу.

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

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

Апокрифический пример

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

Контрпример

Тем не менее, исходный код CPython для C (наиболее распространенная и ссылочная реализация Python) использует gotoоператоры с большим эффектом. Они используются для обхода нерелевантного потока управления внутри функций для достижения конца функций, что делает функции более эффективными без потери читабельности. Это учитывает один из идеалов структурного программирования - иметь единую точку выхода для функций.

Для справки, я считаю, что этот контрпример довольно легко анализировать статически.

Аарон Холл
источник
5
«История Мел», с которой я столкнулся, была связана со странной архитектурой, в которой (1) каждая инструкция на машинном языке делает goto после своей операции, и (2) было ужасно неэффективно размещать последовательность инструкций в последовательных ячейках памяти. И программа была написана на сборке, оптимизированной вручную, а не на скомпилированном языке.
@MilesRout Я думаю, вы могли бы быть правы, но на странице википедии о структурированном программировании сказано: «Наиболее распространенное отклонение, встречающееся во многих языках, - это использование оператора возврата для раннего выхода из подпрограммы. Это приводит к нескольким точкам выхода вместо единой точки выхода, необходимой для структурированного программирования. " - Вы говорите, что это неправильно? У вас есть источник для цитирования?
Аарон Холл
@AaronHall Это не оригинальное значение.
Майлз Рут
11

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

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

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

Вы достаточно взрослые, чтобы знать фразу «для нумерации» ? Вам не нужно это знать, но если вы не знаете, то вы не знакомы с историческим контекстом, в котором предупреждение gotoбыло принципиально актуальным.

Предупреждение против gotoпросто не очень актуально сегодня. С базовым обучением циклам while / for и вызовам функций вы даже не будете думать о выпуске gotoочень часто. Когда вы думаете об этом, у вас, вероятно, есть причина, так что продолжайте.

Но можно ли этим gotoутверждением не злоупотреблять?

Ответ: конечно, им можно злоупотреблять, но его злоупотребление - довольно крошечная проблема в разработке программного обеспечения по сравнению с гораздо более распространенными ошибками, такими как использование переменной, в которой будет использоваться константа, или, например, программирование методом вырезания и вставки (в противном случае известный как пренебрежение к рефакторингу). Я сомневаюсь, что вы в большой опасности. Если вы не используете longjmpили иным образом не переносите управление в удаленный код, если вы думаете, что хотите использовать его gotoили просто хотите попробовать его, просто продолжайте. Вам будет хорошо.

Вы можете заметить отсутствие недавних ужасов, в которых gotoиграет злодея. Большинству или всем из этих историй кажется 30 или 40 лет. Вы стоите на твердой почве, если считаете, что эти истории в основном устарели.

THB
источник
1
Я вижу, что я привлек, по крайней мере, одно снижение. Ну, этого следовало ожидать. Могут прийти и другие отрицательные отзывы. Однако я не собираюсь давать обычный ответ, когда десятилетия опыта программирования научили меня, что в данном случае обычный ответ оказывается неправильным. Во всяком случае, это мое мнение. Я привел свои причины. Читатель может решить.
THB
1
Ваш пост слишком хорош, я не могу понизить комментарии.
Питер Б
7

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

qfwfq
источник
1
Visual Basic 6 и VBA имеют болезненную обработку ошибок, но если вы используете On Error Goto errh, вы можете использовать, Resumeчтобы повторить попытку, Resume Nextпропустить ошибочную строку и Erlузнать, какая это была строка (если вы используете номера строк).
Сис Тиммерман
@CeesTimmerman Я очень мало сделал с Visual Basic, я этого не знал. Я добавлю комментарий об этом.
qfwfq
2
@CeesTimmerman: семантика VB6 «резюме» ужасна, если в подпрограмме возникает ошибка, которая не имеет своего собственного блока при ошибке. Resumeвыполнит эту функцию с самого начала и Resume Nextпропустит все функции, которые еще не были выполнены.
суперкат
2
@supercat Как я уже сказал, это больно. Если вам нужно знать точную строку, вы должны пронумеровать их все или временно отключить обработку ошибок с помощью On Error Goto 0и отладки вручную. Resumeпредназначен для использования после проверки Err.Numberи внесения необходимых корректировок.
Сис Тиммерман
1
Я должен отметить, что в современном языке gotoработает только с помеченными линиями, которые, как представляется, были заменены помеченными циклами .
Сис Тиммерман
7

goto людям сложнее рассуждать, чем другим формам контроля потока.

Программировать правильный код сложно. Трудно писать правильные программы, определить, верны ли программы, сложно, а правильно проверить программы - сложно.

Получить код, который смутно делает то, что вы хотите, легко по сравнению со всем остальным в программировании

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

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

Например, в некоторых языках есть функция сопрограмм. Сопрограммы - нити без ниток; состояние выполнения без потока для его запуска. Вы можете попросить их выполнить, и они могут запустить часть себя, а затем приостановить, передав контроль потока обратно.

«Неглубокие» сопрограммы в языках без поддержки сопрограмм (например, C ++ pre-C ++ 20 и C) возможны с использованием сочетания gotos и ручного управления состоянием. «Глубокие» сопрограммы могут быть выполнены с использованием функций setjmpи longjmpC.

Есть случаи, когда сопрограммы настолько полезны, что написание их вручную и тщательно стоит того.

В случае C ++ они оказываются достаточно полезными, чтобы расширить язык для их поддержки. В gotos и ручное управление состоянием, скрытого уровня абстракции нулевой стоимости, что позволяет программистам писать их без трудностью того , чтобы доказать свою путаницу Гото, void**с, ручное построение / разрушение государства и т.д. правильно.

Goto скрывается за абстракцией более высокого уровня, вроде whileили forили ifили или switch. Эти абстракции более высокого уровня легче доказать и проверить.

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

Заставить компьютер смутно делать то, что вы хотите, в обычных случаях легко . Написание надежно надежного кода сложно . Гото помогает первому намного больше, чем второму; следовательно, «goto считается вредным», так как это признак поверхностно «работающего» кода с глубокими и трудными для отслеживания ошибок. При достаточных усилиях вы все равно можете сделать код с «goto» надежно надежным, поэтому придерживаться правила абсолютного неверно; но, как правило, это хорошо.

Yakk
источник
1
longjmpдопустимо только в C для разматывания стека обратно setjmpв родительскую функцию. (Как разрыв нескольких уровней вложенности, но для вызовов функций вместо циклов). longjjmpк контексту из функции setjmpdone в функции, которая с тех пор вернулась, не разрешено (и я подозреваю, что в большинстве случаев это не сработает). Я не знаком с сопрограммами, но из вашего описания подпрограммы, похожей на поток в пользовательском пространстве, я думаю, что для этого потребуется собственный стек, а также контекст сохраненного регистра, и он longjmpдает только регистр. -сохранение, а не отдельный стек.
Питер Кордес
1
О, я должен был просто гуглить : fanf.livejournal.com/105413.html описывает создание стекового пространства для запуска сопрограммы. (Что, как я подозревал, необходимо использовать просто с помощью setjmp / longjmp).
Питер Кордес
2
@PeterCordes: Реализации не обязаны предоставлять какие-либо средства, с помощью которых код может создать пару jmp_buff, подходящую для использования в качестве сопрограммы. Некоторые реализации указывают средства, с помощью которых код может их создавать, даже если стандарт не требует, чтобы они предоставляли такую ​​возможность. Я бы не стал описывать код, который опирается на такие методы, как «стандартный C», поскольку во многих случаях jmp_buff будет непрозрачной структурой, которая не гарантирует согласованного поведения между различными компиляторами.
суперкат
6

Взгляните на этот код из http://www-personal.umich.edu/~axe/research/Software/CC/CC2/TourExec1.1.f.html, который на самом деле был частью симуляции большой дилеммы заключенного. Если вы видели старый код FORTRAN или BASIC, вы поймете, что это не так уж необычно.

C  Not nice rules in second round of tour (cut and pasted 7/15/93)
   FUNCTION K75R(J,M,K,L,R,JA)
C  BY P D HARRINGTON
C  TYPED BY JM 3/20/79
   DIMENSION HIST(4,2),ROW(4),COL(2),ID(2)
   K75R=JA       ! Added 7/32/93 to report own old value
   IF (M .EQ. 2) GOTO 25
   IF (M .GT. 1) GOTO 10
   DO 5 IA = 1,4
     DO 5 IB = 1,2
5  HIST(IA,IB) = 0

   IBURN = 0
   ID(1) = 0
   ID(2) = 0
   IDEF = 0
   ITWIN = 0
   ISTRNG = 0
   ICOOP = 0
   ITRY = 0
   IRDCHK = 0
   IRAND = 0
   IPARTY = 1
   IND = 0
   MY = 0
   INDEF = 5
   IOPP = 0
   PROB = .2
   K75R = 0
   RETURN

10 IF (IRAND .EQ. 1) GOTO 70
   IOPP = IOPP + J
   HIST(IND,J+1) = HIST(IND,J+1) + 1
   IF (M .EQ. 15 .OR. MOD(M,15) .NE. 0 .OR. IRAND .EQ. 2) GOTO 25
   IF (HIST(1,1) / (M - 2) .GE. .8) GOTO 25
   IF (IOPP * 4 .LT. M - 2 .OR. IOPP * 4 .GT. 3 * M - 6) GOTO 25
   DO 12 IA = 1,4
12 ROW(IA) = HIST(IA,1) + HIST(IA,2)

   DO 14 IB = 1,2
     SUM = .0
     DO 13 IA = 1,4
13   SUM = SUM + HIST(IA,IB)
14 COL(IB) = SUM

   SUM = .0
   DO 16 IA = 1,4
     DO 16 IB = 1,2
       EX = ROW(IA) * COL(IB) / (M - 2)
       IF (EX .LE. 1.) GOTO 16
       SUM = SUM + ((HIST(IA,IB) - EX) ** 2) / EX
16 CONTINUE

   IF (SUM .GT. 3) GOTO 25
   IRAND = 1
   K75R = 1
   RETURN

25 IF (ITRY .EQ. 1 .AND. J .EQ. 1) IBURN = 1
   IF (M .LE. 37 .AND. J .EQ. 0) ITWIN = ITWIN + 1
   IF (M .EQ. 38 .AND. J .EQ. 1) ITWIN = ITWIN + 1
   IF (M .GE. 39 .AND. ITWIN .EQ. 37 .AND. J .EQ. 1) ITWIN = 0
   IF (ITWIN .EQ. 37) GOTO 80
   IDEF = IDEF * J + J
   IF (IDEF .GE. 20) GOTO 90
   IPARTY = 3 - IPARTY
   ID(IPARTY) = ID(IPARTY) * J + J
   IF (ID(IPARTY) .GE. INDEF) GOTO 78
   IF (ICOOP .GE. 1) GOTO 80
   IF (M .LT. 37 .OR. IBURN .EQ. 1) GOTO 34
   IF (M .EQ. 37) GOTO 32
   IF (R .GT. PROB) GOTO 34
32 ITRY = 2
   ICOOP = 2
   PROB = PROB + .05
   GOTO 92

34 IF (J .EQ. 0) GOTO 80
   GOTO 90

70 IRDCHK = IRDCHK + J * 4 - 3
   IF (IRDCHK .GE. 11) GOTO 75
   K75R = 1
   RETURN

75 IRAND = 2
   ICOOP = 2
   K75R = 0
   RETURN

78 ID(IPARTY) = 0
   ISTRNG = ISTRNG + 1
   IF (ISTRNG .EQ. 8) INDEF = 3
80 K75R = 0
   ITRY = ITRY - 1
   ICOOP = ICOOP - 1
   GOTO 95

90 ID(IPARTY) = ID(IPARTY) + 1
92 K75R = 1
95 IND = 2 * MY + J + 1
   MY = K75R
   RETURN
   END

Здесь много вопросов, которые выходят далеко за рамки заявления GOTO; Я искренне думаю, что заявление GOTO было чем-то вроде козла отпущения. Но поток управления здесь абсолютно неясен, и код смешивается таким образом, что очень неясно, что происходит. Даже без добавления комментариев или использования более подходящих имен переменных, если изменить это на блочную структуру без GOTO, будет намного легче читать и следовать.

prosfilaes
источник
4
так верно :-) Может быть стоит упомянуть: в старых версиях FORTAN и BASIC не было блочных структур, так что если предложение THEN не могло содержать одну строку, GOTO была единственной альтернативой.
Кристоф
Текстовые метки вместо цифр или, по крайней мере, комментарии к пронумерованным строкам очень помогли бы.
Сис Тиммерман
Назад в мои дни на Фортране-IV: я ограничил свое использование GOTO для реализации блоков IF / ELSE IF / ELSE, циклов WHILE и BREAK и NEXT в циклах. Кто-то показал мне зло кода спагетти и почему вы должны структурировать, чтобы избежать этого, даже если вам нужно реализовать свои блочные структуры с помощью GOTO. Позже я начал использовать препроцессор (RATFOR, IIRC). Goto не является по сути злым, это просто сверхмощная низкоуровневая конструкция, которая отображается в инструкции ветки ассемблера. Если вам нужно использовать его из-за недостатка языка, используйте его для создания или дополнения правильных структур блоков.
nigel222
@CeesTimmerman: Это код FORTRAN IV для разных сортов сада. Текстовые метки не поддерживались в FORTRAN IV. Я не знаю, поддерживают ли их более поздние версии языка. Комментарии в той же строке, что и код, не были поддержаны в стандартном FORTRAN IV, хотя, возможно, для их поддержки были специфичные для поставщика расширения. Я не знаю, о чем говорит «текущий» стандарт FORTRAN: я давно отказался от FORTRAN и не скучаю по нему. (Самый последний код на Фортране, который я видел, очень похож на PASCAL начала 1970-х.)
John R. Strohm
1
@CeesTimmerman: расширение для конкретного поставщика. Код в целом ДЕЙСТВИТЕЛЬНО стесняется комментариев. Кроме того, в некоторых местах стало общепринятым соглашение о размещении номеров строк только в операторах CONTINUE и FORMAT, чтобы упростить добавление строк позже. Этот код не соответствует этому соглашению.
Джон Р. Штром
0

Одним из принципов поддерживаемого программирования является инкапсуляция . Дело в том, что вы взаимодействуете с модулем / подпрограммой / подпрограммой / компонентом / объектом, используя определенный интерфейс и только этот интерфейс, и результаты будут предсказуемы (при условии, что устройство было эффективно протестировано).

В пределах одной единицы кода применяется тот же принцип. Если вы последовательно применяете принципы структурного или объектно-ориентированного программирования, вы не будете:

  1. создавать неожиданные пути через код
  2. поступать в разделы кода с неопределенными или недопустимыми значениями переменных
  3. выйти из пути кода с неопределенными или недопустимыми значениями переменных
  4. не завершить транзакцию
  5. оставить части кода или данных в памяти, которые фактически мертвы, но не подходят для очистки и перераспределения мусора, потому что вы не сигнализировали об их освобождении, используя явную конструкцию, которая вызывает их освобождение, когда путь управления кодом выходит из модуля

Некоторые из наиболее распространенных симптомов этих ошибок обработки включают утечки памяти, накопление памяти, переполнение указателей, сбои, неполные записи данных, исключения добавления / chg / delete записи, сбои страниц памяти и так далее.

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

jaxter
источник
3
Вы ответили, не упоминая gotoни разу. :)
Роберт Харви
0

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


Причины использовать / не использовать goto :

  • Если вам нужен цикл, вы должны использовать whileили for.

  • Если вам нужно сделать условный переход, используйте if/then/else

  • Если вам нужна процедура, вызовите функцию / метод.

  • Если вам нужно выйти из функции, просто return.

Я могу посчитать по пальцам места, где я видел, gotoиспользовал и использовал правильно.

  • CPython
  • libKTX
  • вероятно, еще несколько

В libKTX есть функция, которая имеет следующий код

if(something)
    goto cleanup;

if(bla)
    goto cleanup;

cleanup:
    delete [] array;

Теперь в этом месте gotoполезно, потому что язык C:

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

Этот вариант использования полезен, потому что в C у нас нет классов, поэтому самый простой способ очистить это использование a goto.

Если бы у нас был тот же код на C ++, больше нет необходимости в goto:

class MyClass{
    public:

    void Function(){
        if(something)
            return Cleanup(); // invalid syntax in C#, but valid in C++
        if(bla)
            return Cleanup(); // invalid syntax in C#, but valid in C++
    }

    // access same members, no need to pass state (compiler do it for us).
    void Cleanup(){

    }



}

К каким ошибкам это может привести? Что-нибудь. Бесконечные циклы, неправильный порядок выполнения, стопорный винт.

Документированный случай - это уязвимость в SSL, которая позволила атакам «Человек посередине», вызванным неправильным использованием goto: вот статья

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

Разработчик игр
источник
2
К одному ответу есть множество комментариев, которые очень четко объясняют, что использование «goto» совершенно не имело отношения к ошибке SSL - ошибка была вызвана небрежным дублированием одной строки кода, поэтому однажды она была выполнена условно, а однажды безоговорочно.
gnasher729
Да, я жду лучшего исторического примера ошибки goto, прежде чем согласиться. Как сказано; Не имеет значения, что находится в этой строке кода; Отсутствие фигурных скобок выполнило бы это и вызвало бы ошибку независимо. Мне любопытно, хотя; что такое "винт стека"?
Акива
1
Пример из кода, над которым я работал ранее в PL / 1 CICS. Программа выпустила экран, состоящий из однолинейных карт. Когда экран был возвращен, он обнаружил массив отправленных карт. Использовал элементы этого, чтобы найти индекс в одном массиве, а затем использовал этот индекс для индекса переменных массива, чтобы выполнить GOTO, чтобы он мог обработать эту отдельную карту. Если отправленный массив карт был неправильным (или был поврежден), он мог попытаться выполнить GOTO с неверной меткой или, что еще хуже, вызвать сбой, поскольку он получил доступ к элементу после конца массива переменных метки. Переписан для использования синтаксиса типа регистра.
Кикстарт