首页 / 知识

关于语言不可知:GOTO仍然被认为是有害的?

2023-04-14 08:58:00

GOTO still considered harmful?

每个人都知道Dijkstra给编辑的信件:转到被认为有害的声明(也就是这里.html脚本和这里的.pdf)并且从那时起就有一个强大的推动,尽可能避开goto声明。虽然可以使用goto来生成不可维护的,庞大的代码,但它仍然保留在现代编程语言中。甚至Scheme中的高级连续控制结构也可以被描述为复杂的goto。

什么情况下可以使用goto?什么时候最好避免?

作为后续问题:C提供了一对函数setjmp和longjmp,它们不仅可以在当前堆栈帧内,而且可以在任何调用帧中进行转换。这些应该被视为像goto一样危险吗?更危险吗?

Dijkstra本人对这个头衔表示遗憾,他对此不负责任。在EWD1308(也是这里.pdf)结束时,他写道:

Finally a short story for the record.
In 1968, the Communications of the ACM
published a text of mine under the
title"The goto statement considered
harmful", which in later years would
be most frequently referenced,
regrettably, however, often by authors
who had seen no more of it than its
title, which became a cornerstone of
my fame by becoming a template: we
would see all sorts of articles under
the title"X considered harmful" for
almost any X, including one titled
"Dijkstra considered harmful". But
what had happened? I had submitted a
paper under the title"A case against
the goto statement", which, in order
to speed up its publication, the
editor had changed into a"letter to
the Editor", and in the process he had
given it a new title of his own
invention! The editor was Niklaus
Wirth.

关于这个主题的经过深思熟虑的经典论文,与Dijkstra的相匹配,是Donald E. Knuth的结构化编程和陈述。阅读都有助于重新建立背景和对主题的非教条性理解。在本文中,Dijkstra对此案的观点得到了报道,甚至更为强烈:

Donald E. Knuth: I believe that by presenting such a
view I am not in fact disagreeing
sharply with Dijkstra's ideas, since
he recently wrote the following:
"Please don't fall into the trap of
believing that I am terribly
dogmatical about [the go to
statement]. I have the uncomfortable
feeling that others are making a
religion out of it, as if the
conceptual problems of programming
could be solved by a single trick, by
a simple form of coding discipline!"


XKCDs GOTO Comic

我的一位同事说,使用GOTO的唯一原因是如果你把自己编程到一个角落,这是唯一的出路。换句话说,提前进行适当的设计,以后您不需要使用GOTO。

我认为这部漫画很精彩地说明了"我可以重组程序的流程,或者使用一点'GOTO'代替。"当你的设计薄弱时,GOTO是一种微弱的出路。迅猛龙捕食弱者。


以下陈述是概括;虽然总是可以恳求例外,但通常(根据我的经验和拙见)并不值得冒这个风险。

  • 无限制地使用内存地址(GOTO或原始指针)提供了太多机会来轻松避免错误。
  • 到达代码中特定"位置"的方式越多,对该系统状态的信心就越不自信。 (见下文。)
  • 结构化编程IMHO不是关于"避免GOTO",而是关于使代码结构与数据结构相匹配。例如,重复数据结构(例如,阵列,顺序文件等)由重复的代码单元自然地处理。具有内置结构(例如,while,for,until,for-each等)允许程序员避免重复相同的陈词滥调代码模式的乏味。
  • 即使GOTO是低级实现细节(并非总是如此!),它低于程序员应该考虑的级别。有多少程序员在原始二进制文件中平衡他们的个人支票簿?有多少程序员担心磁盘上的哪个扇区包含特定记录,而不是仅仅为数据库引擎提供密钥(如果我们真的按照物理磁盘扇区编写了所有程序,那么有多少种方法会出错)?
  • 以上脚注:

    关于第2点,请考虑以下代码:

    1
    2
    a = b + 1
    /* do something with a */

    在代码中的"做某事"点,我们可以高度自信地说明a大于b。 (是的,我忽略了未整数溢出的可能性。让我们不要陷入一个简单的例子。)

    另一方面,如果代码以这种方式读取:

    1
    2
    3
    4
    5
    6
    7
    8
    ...
    goto 10
    ...
    a = b + 1
    10: /* do something with a */
    ...
    goto 10
    ...

    获得标签10的多种方式意味着我们必须更加努力地对那时ab之间的关系充满信心。 (事实上??,在一般情况下,它是不可判定的!)

    关于第4点,代码中"走向某个地方"的整个概念只是一个隐喻。除了电子和光子(废热)之外,CPU内部的任何地方都没有"真正"的"走向"。有时我们会放弃另一个更有用的隐喻。我记得曾经(几十年前)遇到过一种语言

    1
    2
    3
    4
    5
    if (some condition) {
      action-1
    } else {
      action-2
    }

    通过将action-1和action-2编译为out-of-line无参数例程,然后使用单个双参数VM操作码,使用条件的布尔值来调用其中一个,从而在虚拟机上实现。这个概念只是"选择现在要调用的东西",而不是"去这里或去那里"。再一次,只是一个隐喻的变化。


    有时在单个函数中使用GOTO替代异常处理是有效的:

    1
    2
    3
    4
    5
    6
    7
    8
    if (f() == false) goto err_cleanup;
    if (g() == false) goto err_cleanup;
    if (h() == false) goto err_cleanup;

    return;

    err_cleanup:
    ...

    COM代码似乎经常陷入这种模式。


    我只记得使用过一次goto。我有一系列五个嵌套计数循环,我需要能够根据某些条件从内部早期打破整个结构:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    for{
      for{
        for{
          for{
            for{
              if(stuff){
                GOTO ENDOFLOOPS;
              }
            }
          }
        }
      }
    }

    ENDOFLOOPS:

    我可以轻松地声明一个布尔中断变量并将其用作每个循环的条件的一部分,但在这个实例中我决定GOTO同样实用且同样可读。

    没有迅猛龙攻击我。


    我们已经讨论了这个问题,我坚持自己的观点。

    此外,我厌倦了将高级语言结构描述为"伪装的goto"的人,因为他们显然根本没有说明这一点。例如:

    Even the advanced continuation control structure in Scheme can be described as a sophisticated goto.

    这完全是胡说八道。每个控制结构都可以用goto来实现,但是这种观察完全是微不足道的,毫无用处。 goto因其积极影响而被认为是有害的,但由于其负面影响而已被结构化编程所消除。

    同样地,说"GOTO是一种工具,并且作为所有工具,它可以被使用和滥用"完全不合适。没有现代建筑工人会使用岩石并声称它"是一种工具。"岩石已被锤子取代。 goto已被控制结构取代。如果建筑工人在没有锤子的情况下被困在野外,当然他会使用岩石代替。如果程序员必须使用没有特征X的劣质编程语言,那么当然她可能不得不使用goto。但如果她在其他任何地方使用它而不是相应的语言功能,她显然不能正确理解语言并错误地使用它。它真的很简单。


    Goto在我的列表中非常低,只是为了它而包含在程序中。这并不意味着它是不可接受的。

    Goto可以用于状态机。循环中的switch语句(按典型重要性顺序):( a)实际上不代表控制流,(b)丑陋,(c)取决于语言和编译器,可能效率低下。所以你最终会为每个状态编写一个函数,并执行"return NEXT_STATE;"之类的操作。甚至看起来像goto。

    当然,很难以一种易于理解的方式对状态机进行编码。然而,使用goto没有任何困难,并且通过使用替代控制结构不能减少任何困难。除非您的语言具有"状态机"构造。我没有。

    在极少数情况下,您的算法在通过一组有限的允许转换(gotos)连接的节点(状态)的路径中最容易理解,而不是通过任何更具体的控制流(循环,条件,诸如此类) ),然后在代码中应该是明确的。你应该绘制一个漂亮的图表。

    setjmp / longjmp可以很好地实现异常或类似异常的行为。虽然没有得到普遍赞扬,但例外通常被认为是"有效"的控制结构。

    setjmp / longjmp比goto"更危险",因为它们更难以正确使用,从不介意理解。

    There never has been, nor will there
    ever be, any language in which it is
    the least bit difficult to write bad
    code. -- Donald Knuth.

    从C中取出goto不会让在C中编写好的代码变得更容易。实际上,它宁愿忽略C应该能够作为一种美化的汇编语言。

    接下来它将是"被认为有害的指针",然后"鸭子打字被认为是有害的"。然后,当他们拿走你不安全的编程结构时,谁会留下来为你辩护?嗯?


    在Linux中:在内核陷阱中使用goto In Kernel Code,与Linus Torvalds和一个关于在Linux代码中使用GOTO的"新人"进行了讨论。那里有一些非常好的点,而Linus穿着那种平常的傲慢:)

    一些段落:

    Linus:"No, you've been brainwashed by
    CS people who thought that Niklaus
    Wirth actually knew what he was
    talking about. He didn't. He doesn't
    have a frigging clue."

    -

    Linus:"I think goto's are fine, and
    they are often more readable than
    large amounts of indentation."

    -

    Linus:"Of course, in stupid languages
    like Pascal, where labels cannot be
    descriptive, goto's can be bad."


    在C中,goto仅在当前函数的范围内工作,这往往会定位任何潜在的错误。 setjmplongjmp更危险,非本地,复杂且依赖于实现。然而,在实践中,它们太模糊,并且不常见导致许多问题。

    我相信C中goto的危险性被夸大了。请记住,原始的goto参数发生在像老式BASIC这样的语言时代,初学者会像这样编写意大利面条代码:

    1
    3420 IF A > 2 THEN GOTO 1430

    这里Linus描述了goto的适当使用:http://www.kernel.org/doc/Documentation/CodingStyle(第7章)。


    今天,很难看到关于GOTO声明的大问题,因为"结构化编程"人们大多赢得了辩论,而今天的语言有足够的控制流结构来避免GOTO

    计算现代C程序中GOTO的数量。现在添加breakcontinuereturn语句的数量。此外,添加使用ifelsewhileswitchcase的次数。这就是当你在Dijkstra写信时,如果你在1968年写FORTRAN或BASIC时你的程序会有多少GOTO s。

    当时的编程语言缺乏控制流程。例如,在最初的达特茅斯基础:

    • if语句没有else。如果你想要一个,你必须写:

      1
      2
      3
      4
      5
      6
      100 IF NOT condition THEN GOTO 200
      ...stuff to do if condition is true...
      190 GOTO 300
      200 REM else
      ...stuff to do if condition is false...
      300 REM end if
    • 即使你的if语句不需要else,它仍然限于一行,通常由GOTO组成。

    • 没有DO...LOOP声明。对于非FOR循环,您必须以明确的GOTOIF...GOTO结束循环到开头。

    • 没有SELECT CASE。你必须使用ON...GOTO

    所以,你在程序中得到了很多GOTO s。你不能依赖于GOTO s在单个子程序中的限制(因为GOSUB...RETURN是子程序的一个弱概念),所以这些GOTO可以去任何地方。显然,这使得控制流程很难遵循。

    这就是反GOTO运动的来源。


    在某些情况下,Go To可以为"真正的"异常处理提供一种替代。考虑:

    1
    2
    3
    4
    5
    6
    ptr = malloc(size);
    if (!ptr) goto label_fail;
    bytes_in = read(f_in,ptr,size);
    if (bytes_in=<0) goto label_fail;
    bytes_out = write(f_out,ptr,bytes_in);
    if (bytes_out != bytes_in) goto label_fail;

    显然,这段代码被简化为占用更少的空间,所以不要太过于挂在细节上。但是考虑一下我在生产代码中看到过多次的替代方法,编码人员为了避免使用goto而荒谬的长度:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    success=false;
    do {
        ptr = malloc(size);
        if (!ptr) break;
        bytes_in = read(f_in,ptr,size);
        if (count=<0) break;
        bytes_out = write(f_out,ptr,bytes_in);
        if (bytes_out != bytes_in) break;
        success = true;
    } while (false);

    从功能上讲,这段代码完全相同。实际上,编译器生成的代码几乎完全相同。然而,在程序员热衷于安抚Nogoto(可怕的学术谴责之神)的热情中,这个程序员完全打破了while循环所代表的基本习惯,并对代码的可读性做了一个真实的数字。这不是更好。

    所以,故事的寓意是,如果你发现自己为了避免使用goto而采取真正愚蠢的事情,那就不要了。


    Donald E. Knuth在1992年的CSLI"Literate Programming"一书中回答了这个问题。在p。 17有一篇文章"带有goto语句的结构化编程"(PDF)。我认为这篇文章也可能已在其他书籍中发表过。

    这篇文章描述了Dijkstra的建议,并描述了这种情况的有效性。但他也提供了许多反例(问题和算法),这些例子只能使用结构化循环来轻松复制。

    本文包含问题的完整描述,历史,示例和反例。


    转到认为有帮助。

    我在1975年开始编程。对于20世纪70年代的程序员来说,"转向被认为是有害的"这些词或多或少地表示具有现代控制结构的新编程语言值得尝试。我们确实尝试过新语言。我们很快转换了。我们再也没有回去过。

    我们再也没有回去,但是,如果你年轻,那么你从来没有去过那里。

    现在,除了作为程序员年龄的指标之外,古代编程语言的背景可能不是很有用。尽管如此,年轻的程序员缺乏这种背景,因此他们不再理解在引入时向其目标受众传达的"转向被认为有害"的口号。

    一个人不理解的标语并不是很有启发性。最好忘记这样的口号。这样的口号没有帮助。

    然而,这个特殊的口号"Goto被认为是有害的"已经成为了自己的不死生命。

    可以转到不被滥用?答:当然可以,但那又怎样?实际上每个编程元素都可能被滥用。例如,卑微的bool比我们想要相信的人更容易被滥用。

    相比之下,我不记得自1990年以来遇到一个单一的,实际的goto滥用实例。

    goto最大的问题可能不是技术问题,而是社交问题。有时候不太了解的程序员似乎觉得弃用goto会让他们听起来很聪明。您可能不得不满足这些程序员。这就是生活。

    关于goto今天最糟糕的事情是它使用不够。


    被Jay Ballou吸引并添加一个答案,我将加上0.02英镑。如果Bruno Ranschaert还没有这样做,我会提到Knuth的"使用GOTO语句进行结构化编程"一文。

    我没有看过的一件事就是在Fortran教科书中讲授的一些代码,虽然不常见,但却很常见。像DO循环和开放编码子程序的扩展范围(记住,这将是Fortran II,Fortran IV或Fortran 66 - 而不是Fortran 77或90)。语法细节至少有可能是不精确的,但概念应该足够准确。每种情况下的片段都在一个函数内。

    请注意,Kernighan和Plauger撰写的优秀但过时(绝版)的书"The Programming of Programming Style,2nd Edn"包含了一些现实生活中的GOTO滥用例子,这些例子来自其时代(70年代后期)的编程教科书。但是,下面的材料不是那本书。

    DO循环的扩展范围

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
           do 10 i = 1,30
               ...blah...
               ...blah...
               if (k.gt.4) goto 37
    91         ...blah...
               ...blah...
    10     continue
           ...blah...
           return
    37     ...some computation...
           goto 91

    这种废话的一个原因是好老式的打卡。您可能会注意到标签(很好地不按顺序,因为这是规范样式!)在第1列(实际上,它们必须在第1-5列中),代码在第7-72列中(第6列是延续标记栏)。第73-80列将被赋予序列号,并且有机器将打卡机卡片分类为序列号顺序。如果您的程序在顺序卡上并且需要在循环中间添加一些卡(行),则必须在这些额外行之后重新启动所有内容。但是,如果你用GOTO的东西替换了一张卡片,你可以避免重新测序所有的卡片 - 你只需要在例程结束时用新的序列号将新卡片塞进去。将其视为"绿色计算"的第一次尝试 - 节省打卡(或者更具体地说,节省重新输入劳动力 - 并节省相应的重新加密错误)。

    哦,你可能还会注意到我在欺骗而不是大喊大叫 - Fortran IV通常用大写字母写成。

    开放编码的子程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
           ...blah...
           i = 1
           goto 76
    123    ...blah...
           ...blah...
           i = 2
           goto 76
    79     ...blah...
           ...blah...
           goto 54
           ...blah...
    12     continue
           return
    76     ...calculate something...
           ...blah...
           goto (123, 79) i
    54     ...more calculation...
           goto 12

    标签76和54之间的GOTO是计算goto的版本。如果变量i的值为1,则转到列表中的第一个标签(123);如果它的值为2,则转到第二个,依此类推。从76到计算goto的片段是开放编码的子例程。它是一段执行的代码,就像一个子程序,但写在一个函数体中。 (Fortran还具有语句功能 - 这些功能是嵌入在单行上的功能。)

    有比构成的goto更糟糕的构造 - 你可以为变量分配标签,然后使用指定的goto。谷歌搜索分配goto告诉我它已从Fortran 95中删除。用白色结构编程革命,可以说是公开的Dijkstra的"GOTO Considered Harmful"字母或文章。

    如果不了解Fortran所做的各种事情(以及其他语言,其中大多数已被正确地撇开),我们新手很难理解Dijkstra正在处理的问题的范围。哎呀,直到那封信发表十年后我才开始编程(但我确实不幸在Fortran IV中编程了一段时间)。


    没有GOTO认为有害的东西。

    GOTO是一种工具,作为所有工具,它可以被使用和滥用。

    然而,编程世界中有许多工具比使用更容易被滥用,而GOTO就是其中之一。 Delphi的WITH语句是另一个。

    就个人而言,我不会在典型代码中使用任何一种,但我已经保证了GOTO和WITH的奇怪用法,并且替代解决方案将包含更多代码。

    最好的解决方案是编译器只是警告你关键字被污染了,你必须在语句周围填写一些pragma指令来摆脱警告。

    这就像告诉你的孩子不要用剪刀跑。剪刀也不错,但使用它们可能不是保持健康的最佳方法。


    自从我开始在linux内核中做了一些事情以来,getos并没有像以前那样困扰我。起初我有点害怕看到他们(内核人员)在我的代码中添加了getos。我已经习惯于在某些有限的环境中使用gotos,现在偶尔也会使用它们。通常,它是一个转到函数末尾进行某种清理和纾困的goto,而不是在函数中的几个位置复制相同的清理和挽救。通常情况下,它不足以传递给另一个功能 - 例如释放一些局部(k)malloc'ed变量是一个典型的例子。

    我编写的代码只使用了setjmp / longjmp一次。它是在MIDI鼓音序器程序中。回放发生在与所有用户交互不同的进程中,回放过程使用共享内存和UI进程来获取回放所需的有限信息。当用户想要停止播放时,播放过程只是做了一个"回到开始"的longjmp重新开始,而不是在用户希望它停止时执行的任何地方的一些复杂的展开。它工作得很好,很简单,在这种情况下我从来没有遇到任何与之相关的问题或错误。

    setjmp / longjmp有他们的位置 - 但是那个地方是你不可能访问的地方,但很长一段时间。

    编辑:我只看了代码。它实际上是我使用的siglongjmp(),而不是longjmp(不是说这是一个大问题,但我忘记了siglongjmp甚至存在。)


    它永远不会,只要你能够为自己思考。


    如果你用C编写VM,事实证明使用(gcc)计算得到的结果是这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    char run(char *pc) {
        void *opcodes[3] = {&&op_inc, &&op_lda_direct, &&op_hlt};
        #define NEXT_INSTR(stride) goto *(opcodes[*(pc += stride)])
        NEXT_INSTR(0);
        op_inc:
        ++acc;
        NEXT_INSTR(1);
        op_lda_direct:
        acc = ram[++pc];
        NEXT_INSTR(1);
        op_hlt:
        return acc;
    }

    比循环内的传统开关工作得快得多。


    因为goto可用于混淆元编程

    goto既是高级也是低级控制表达式,因此它没有适合大多数问题的适当设计模式。

    它是低级别的,因为goto是一种原始操作,可以实现更高级的内容,如whileforeach等。

    从某种意义上讲,它是高级别的,它以一种不间断的方式以明确的顺序执行代码,除了结构化循环,并将其转换为具有足够goto s的逻辑片段。 ,一个动态重新组装的逻辑包。

    因此,goto有一个平淡而邪恶的一面。

    平淡无奇的一面是,向上指向的goto可以实现一个完全合理的循环,向下指向的goto可以完成一个完全合理的breakreturn。当然,实际的whilebreakreturn会更具可读性,因为穷人不需要模拟goto的效果才能获得全局。所以,一般来说一个坏主意。

    邪恶的一方涉及一个例程,不使用goto进行while,break或return,而是将其用于所谓的意大利面条逻辑。在这种情况下,goto-happy开发人员正在从goto的迷宫中构建代码片段,理解它的唯一方法是在整体上模拟它,当有很多goto时,这是一个非常累人的任务。我的意思是,想象一下评估代码的麻烦,其中else不完全是if的反转,其中嵌套的if可能允许在外部if等被拒绝的某些东西等。

    最后,为了真正涵盖这一主题,我们应该注意到,除了Algol之外,基本上所有早期语言最初只根据其if-then-else版本制作单个语句。因此,执行条件块的唯一方法是使用反条件在goto周围。疯了,我知道,但我读过一些陈旧的规格。请记住,第一台计算机是用二进制机器代码编程的,所以我认为任何一种HLL都是救星;我猜他们对于HLL的具体功能并不太挑剔。

    说完所有我曾经把一个goto粘贴到我编写的每个程序中"只是为了惹恼纯粹主义者"。


    拒绝将GOTO声明用于程序员就像告诉木匠不要使用锤子一样,因为它可能会在锤击钉子时损坏墙壁。一个真正的程序员知道如何以及何时使用GOTO。我跟在其中一些所谓的"结构化程序"后面我看到这样的Horrid代码只是为了避免使用GOTO,我可以拍摄程序员。好吧,为了防御另一方,我一次又一次地看到了一些真正的意大利面条代码,那些程序员也应该被拍摄。

    这里只是我发现的一个代码示例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
      YORN = ''
      LOOP
      UNTIL YORN = 'Y' OR YORN = 'N' DO
         CRT 'Is this correct? (Y/N) : ':
         INPUT YORN
      REPEAT
      IF YORN = 'N' THEN
         CRT 'Aborted!'
         STOP
      END

    - - - - - - - - - - - -要么 - - - - - - - - - - -

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10:  CRT 'Is this Correct (Y)es/(N)o ':

         INPUT YORN

         IF YORN='N' THEN
            CRT 'Aborted!'
            STOP
         ENDIF
         IF YORN<>'Y' THEN GOTO 10

    "在此链接http://kerneltrap.org/node/553/2131"

    具有讽刺意味的是,消除goto引入了一个错误:省略了spinlock调用。


    原始论文应该被认为是"无条件的GOTO被认为是有害的"。它特别提倡基于条件(if)和迭代(while)结构的编程形式,而不是早期代码常见的测试和跳转。 goto在某些语言或情况下仍然有用,因为不存在适当的控制结构。


    关于我同意Goto可以使用的唯一地方是当你需要处理错误时,每个特定点发生错误都需要特殊处理。

    例如,如果您正在抓取资源并使用信号量或互斥量,则必须按顺序抓取它们,并且应始终以相反的方式释放它们。

    有些代码需要非常奇怪的模式来获取这些资源,并且您不能只编写一个易于维护和理解的控制结构来正确处理这些资源的获取和释放以避免死锁。

    它总是可以在没有goto的情况下正确完成,但在这种情况下和其他一些Goto实际上是更好的解决方案,主要是为了可读性和可维护性。

    -亚当


    一个现代的GOTO用法是由C#编译器为yield yield定义的枚举数创建状态机。

    GOTO应该由编译器而不是程序员使用。


    如果GOTO本身是邪恶的,那么编译器就是邪恶的,因为它们会产生JMP。如果跳进一段代码,特别是在指针之后,本身就是邪恶的,那么RETurn指令就是邪恶的。相反,邪恶有可能被滥用。

    有时我不得不编写必须跟踪许多对象的应用程序,其中每个对象必须遵循复杂的状态序列以响应事件,但整个事情肯定是单线程。一个典型的状态序列,如果用伪代码表示,将是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    request something
    wait for it to be done
    while some condition
        request something
        wait for it
        if one response
            while another condition
                request something
                wait for it
                do something
            endwhile
            request one more thing
            wait for it
        else if some other response
            ... some other similar sequence ...
        ... etc, etc.
    endwhile

    我确定这不是新的,但我在C(++)中处理它的方式是定义一些宏:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #define WAIT(n) do{state=(n); enque(this); return; L##n:;}while(0)
    #define DONE state = -1

    #define DISPATCH0 if state < 0) return;
    #define DISPATCH1 if(state==1) goto L1; DISPATCH0
    #define DISPATCH2 if(state==2) goto L2; DISPATCH1
    #define DISPATCH3 if(state==3) goto L3; DISPATCH2
    #define DISPATCH4 if(state==4) goto L4; DISPATCH3
    ... as needed ...

    然后(假设状态最初为0),上面的结构化状态机变成结构化代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    {
        DISPATCH4; // or as high a number as needed
        request something;
        WAIT(1); // each WAIT has a different number
        while (some condition){
            request something;
            WAIT(2);
            if (one response){
                while (another condition){
                    request something;
                    WAIT(3);
                    do something;
                }
                request one more thing;
                WAIT(4);
            }
            else if (some other response){
                ... some other similar sequence ...
            }
            ... etc, etc.
        }
        DONE;
    }

    对此有一个变化,可以有CALL和RETURN,因此一些状态机可以像其他状态机的子程序一样工作。

    这不寻常吗?是。是否需要维护者的一些学习?是。这种学习是否有成效?我认同。如果没有跳转到块的GOTO可以做到吗?不。


    直到C和C ++(以及其他罪魁祸首)标记了中断并继续,goto将继续发挥作用。


    我避免使用它,因为同事或经理无疑会在代码审查中或在遇到错误时对其使用提出质疑。虽然我认为它有用(例如错误处理案例) - 但是你会遇到其他开发人员,他们会遇到某种类型的问题。

    这不值得。


    几乎所有可以使用goto的情况,你都可以使用其他结构来做同样的事情。无论如何,编译器都使用Goto。

    我个人从不明确地使用它,也不需要。


    我从这里得到的任何答案中都没有看到的一点是,'goto'解决方案通常比经常提到的结构化编程解决方案更有效。

    考虑多嵌套循环的情况,使用'goto'而不是一堆if(breakVariable)部分显然更有效。解决方案"将你的循环置于函数中并使用返回"通常是完全不合理的。在循环使用局部变量的可能情况下,您现在必须通过函数参数传递它们,可能会处理由此产生的额外麻烦。

    现在考虑清理案例,我经常使用它,并且很常见,因为可能对许多语言中没有的try {} catch {}结构负责。完成相同操作所需的检查和额外变量的数量远远超过跳转的一个或两个指令,而且额外的功能解决方案根本不是解决方案。你无法告诉我,它更易于管理或更具可读性。

    现在代码空间,堆栈使用和执行时间在许多情况下对许多程序员来说可能不够重要,但是当你在一个只有2KB代码空间的嵌入式环境中工作时,需要50个字节的额外指令来避免明确定义'goto'是可笑的,这并不像许多高级程序员所认为的那样罕见。

    "goto有害"的说法对于向结构化编程非常有帮助,即使它总是过于泛化。在这一点上,我们都听到它足够警惕使用它(我们应该)。当它显然是工作的正确工具时,我们不需要害怕它。


    我实际上发现自己被迫使用goto,因为我真的想不出更好(更快)的方式来编写这段代码:

    我有一个复杂的对象,我需要对它进行一些操作。如果对象处于一种状态,那么我可以做一个快速版本的操作,否则我不得不做一个慢速版本的操作。事情是,在某些情况下,在慢速操作的中间,有可能意识到这可以通过快速操作完成。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    SomeObject someObject;    

    if (someObject.IsComplex())    // this test is trivial
    {
        // begin slow calculations here
        if (result of calculations)
        {
            // just discovered that I could use the fast calculation !
            goto Fast_Calculations;
        }
        // do the rest of the slow calculations here
        return;
    }

    if (someObject.IsmediumComplex())    // this test is slightly less trivial
    {
        Fast_Calculations:
        // Do fast calculations
        return;
    }

    // object is simple, no calculations needed.

    这是一个速度关键的实时UI代码,所以老实说我认为GOTO在这里是合理的。

    雨果


    您可以使用它来破坏深度嵌套的循环,但大多数情况下,您的代码可以重构为更清晰而没有深层嵌套循环。


    是的,GOTO仍然被认为是有害的。当您发现自己处于使用GOTO可能有效的罕见情况时,您应该对自己的编程技能充满信心,而不需要对其他人进行验证。任何类似GOTO的函数都可以让你在GOTO允许的范围内跳得更远,应该被认为比GOTO更危险。


    GOTO就像一把台锯,在采取适当的安全措施时非常有用。

    我认为它有害,因为大多数初学者都会因桌锯和GOTO而失去手指。

    在某些情况下,它是控制流量的唯一方法,但可以避免这些情况。


    C ++包含构造函数和析构函数。这允许将模式称为RAII(资源分配是初始化)。基本上,您创建一个本地堆栈变量,创建堆栈变量的操作会打开一个文件,分配内存,锁定互斥锁,或以其他方式获取以后必须释放的资源。

    当变量超出范围时,析构函数将运行并释放资源。

    C没有此功能。但是你仍然经常需要在函数的开头获取资源,并在最后释放它们。

    您的函数可能有一个或多个错误条件导致它提前返回。您不想复制资源发布代码。解决方案是使用goto。

    例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    int
    foo(const char *arg)
    {
        char *argcopy = strdup(arg);

        if (!isvalid(argcopy))
            goto out1;

        FILE *myfile = fopen(argcopy,"r");
        if (myfile == NULL)
          goto out1;

        char bytes[10];
        if (fread(bytes, sizeof(bytes), 1, myfile) != sizeof(mybytes))
            goto out2;

        /* do some actual work */
        /* .... */
        /* end of actual work */

        out2:
        fclose(myfile);

        out1:
        free(argcopy);

        return 0;
     }

    在我看到的每个平台上,高级控制结构都是低级别的(跳转)。例如,Java虚拟机有一个跳转字节代码,但没有if,else,while,for等等。

    其中一些编译器为简单的条件块创建意大利面条代码。

    为了回答你的问题,goto仍然被认为有害的人认为是有害的。 Goto很容易失去结构化编程的优势。

    最后,这是你的计划;因此你的决定。我建议你不要使用goto,直到你能够自己回答你的问题,但是在特定问题的背景下。


    虽然我认为最好避免在几乎任何情况下使用,但也有例外。
    例如,我见过的一个地方goto语句是优雅的解决方案,与其他语言相比,更复杂的方法是为解释器实现尾调用elimintation。


    goto可能很有用,下面是用c++编写的非常强大的开源国际象棋引擎鳕鱼的例子。 goto只是跳过一些条件检查(效率增益),如果不是goto语句,程序将不得不这样做。如果goto语句标签位于goto声明之后,则它们非常无害且可读。


    基本的想法是goto给你太多的自由去做你不想做的事情。它可能会导致与goto语句无关的地方出现错误,因此会使代码维护变得更加困难。如果你认为你需要一个goto语句,那你就错了:)你应该重新考虑你的代码构造。这就是为什么现代编程语言已经付出了很多努力来为您提供可读,可维护的流控制结构和异常处理机制。

    我也不同意lassevk。由于goto被滥用而不是正确使用,我相信它在设计良好的语言中没有地位。即使对于goto的"理想"用途,也应该首选其他需要更多代码的方法。

    总而言之,是的它仍然被认为是有害的。


    这本身并不是坏事;当相同的逻辑可以用另一种方式更清楚地表达时使用goto是不好的。它可能使代码很难遵循并使维护变得困难。只是去看看Bad Old Days的Basic中的一些程序作为例子。

    在我看来,在像C#这样的现代语言中,我们不应该在正常情况下需要goto。如果我发现自己使用它,通常表明我需要重新思考我的逻辑 - 几乎可以肯定的是,使用普通代码流语句表达相同代码的方式更为清晰。

    也就是说,goto的特殊用途非常有用(我发现自己对没有它的语言感到烦恼)。我主要在C中使用它来打破多级循环,或者用于错误处理;我相信C#具有语言功能,这意味着您不必这样做。 (它在生成自动生成的代码时也非常有用,但这并不是大多数人在现实生活中遇到的事情。)

    goto还有另一个问题,纯粹是政治问题:很多人讨厌它,并且在代码中使用它,即使是合理的,也可能导致问题。如果这是分配代码,那么是,重写它,或者你可能会被标记下来。否则我会倾向于将它留在下一次你需要对该部分进行维护。


    goto有一些问题。一个是很难看出代码是如何流动的。由于花括号,更容易看到if-block,但是goto会隐藏你。此外,虽然和if也基本上是getos,但它们有助于解释为什么你在代码中来回跳转。有了常规的goto,你必须自己拼凑。

    作为一个练习,尝试编写一些代码来计算斐波那契序列,看看你完成后阅读的难度。

    如果您打算使用该代码,那么我建议您编写一些单元测试并重写它。否则,就这样吧。

    所有这一切,有时,出于性能原因,使用goto可能是合适的。


    有一次,在编程生涯的早期,我创建了一个程序,该程序由链中的一系列函数组成,其中每个函数都称其成功条件和完成。

    这是一个可怕的污泥,有多个严重的问题,最严重的是没有任何功能可以终止,直到其下的所有功能都终止。

    但它很快就被开发出来,对于它设计要解决的有限问题很有效,并且明确地显示了程序的逻辑和流程,当我重构并扩展它以包含在另一个项目中时,它运行良好。

    我的投票是在有意义的时候使用它,并在方便的时候尽快重构它。


    在我看来,"转向有害"更多的是关于状态的封装和一致性而不是其他任何东西。

    许多代码,甚至是"oo"代码,都像任何意大利面条代码一样,具有糟糕的凌乱状态封装。

    "goto被认为有害"的问题在于它让程序员只关注机械规则而不理解应该可用的唯一流量控制是返回方法的印象,这很容易导致传递很多状态通过引用 - 并且直接导致缺乏状态封装,"被认为有害"的东西试图摆脱。

    按照典型的'OO'代码库中的控制流程告诉我,我们仍然没有意大利面条代码....(顺便说一句,我不是说'馄饨'代码通常会引起如此多的仇恨 - 馄饨代码的执行路径通常非常简单,即使对象关系不是很明显)。

    或者,换句话说,如果每个子程序只修改本地状态,那么除了通过那个子程序(或至少是那个对象)之外,只有当每个子程序修改本地状态时,避免使用有利于作为子程序的所有内容才有用。


    许多现代编程语言使用他们的编译器来强制限制GOTO的使用 - 这减少了潜在的风险。例如,C#将不允许您使用GOTO从其外部跳入循环体。文档中提到了限制。

    这是GOTO有时比以前更安全的一个例子。

    在某些情况下,GOTO的使用与从函数早期返回(即早退出循环)相同。无论如何都可以提出良好的形式。


    使用goto使编写"意大利面条代码"变得非常容易,这种代码不是特别易于维护。要遵循的最重要的规则是编写可读代码,但当然这取决于项目的目标是什么。作为避免goto的"最佳实践"是一个好主意。这是极端编程类型称为"代码味道"的东西,因为它表明你可能做错了什么。在循环时使用break非常类似于goto,除了它不是goto,但是再次表明代码可能不是最佳的。这就是为什么,我相信,找不到更多现代编程漏洞也很重要,这些漏洞本质上是一个不同名称的转换。


    我只需要在Basic(即VB,VBScript等)和批处理文件中使用它。然后我只用它来处理错误。在Basic中,我倾向于只使用"on error goto"。在批处理文件中,我必须使用它,因为没有else命令。然后我只将它们用作向前跳转到有意义的标签。


    在一个完美的世界里,我们永远不需要GOTO。但是,我们生活在一个不完美的世界。我们没有可以实现的每个控制结构的编译器。有时我觉得使用GOTO比使用不存在的控制结构更好。

    最常见的(不是常见的)是循环和半结构。你总是执行第一部分,也许你执行剩下的部分然后再回去再做第一部分。当然,您可以在while循环中使用布尔标志来实现它,但我不喜欢这个答案,因为在我看来它不太清楚。当你看到类似的东西时:

    1
    2
    3
    4
    5
    6
    7
    8
    loop:
      GetSomeData;
      if GotData then
         Begin
            ProcessTheData;
            StoreTheResult;
            Goto Loop;
         End;

    对我而言比它更清楚

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Repeat
      GetSomeData;
      Flag := GotData;
      if Flag then
        Begin
          ProcessTheData;
          StoreTheResult;
        End;
    Until Not Flag;

    而且有时候

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Function GotTheData;

    Begin
      GetSomeData;
      Result := GotData;
    End;

    While GotTheData do
      Begin
        ProcessTheData;
        StoreTheResult;
      End;

    这不是一个可行的答案,我坚信代码应该清楚。如果我必须发表评论来解释代码在做什么,我会考虑是否可以使代码更清晰并删除注释。


    在生成C状态机时,使用GOTO会很好。我永远不会在手写代码中使用GOTO -"现代"语言结构使它完全没必要。

    setjmp / longjmp构造在某些情况下很有用(当缺少"true"异常时,或者当你实现类似Chicken方案的东西时),但它在"普通"编程中没有位置。


    计算得到的调度,通常比非常大的switch语句更容易理解。

    对于错误和共线,我认为setcontex或setjmp(如果可用)是"更好"。


    Java String类源代码中的跳转示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    int firstUpper;

    /* Now check if there are any characters that need to be changed. */
    scan: {
        for (firstUpper = 0 ; firstUpper < count; ) {
             char c = value[offset+firstUpper];
             if ((c >= Character.MIN_HIGH_SURROGATE) &&
                     (c <= Character.MAX_HIGH_SURROGATE)) {
                 int supplChar = codePointAt(firstUpper);
                 if (supplChar != Character.toLowerCase(supplChar)) {
                      break scan;
                 }
                 firstUpper += Character.charCount(supplChar);
             } else {
                 if (c != Character.toLowerCase(c)) {
                      break scan;
                 }
                 firstUpper++;
             }
         }
         return this;
    }
    [... subsequent use of firstUpper ...]

    这可以用很少的开销重写,例如:

    1
    2
     int firstUpper = indexOfFirstUpper();
     if (firstUpper < 0) return this;

    即使在现代语言中,即使我实际上并不喜欢使用gotos,但我认为在许多情况下这些都是可以接受的,在像这样的低级情况下,对我来说看起来更好(并且它比仅退出更多一点)循环)。

    无意复活宗教战争。


    看看这个,它是一个很好用的GoTo,但在一个带垃圾收集器的语言中,我认为使用GoTo的唯一原因是混淆你的代码(混淆器工具使用GoTo隐藏他们的代码)


    声明语言脚本维护

    最新内容

    相关内容

    猜你喜欢