每个人都知道Dijkstra的《致编辑的信》:goto语句被认为是有害的(这里。html transcript和这里。pdf),从那时起,就有一种强大的推动力,尽可能避免使用goto语句。虽然可以使用goto来生成不可维护的、庞大的代码,但它仍然存在于现代编程语言中。即使Scheme中先进的连续控制结构也可以被描述为复杂的后向。

在什么情况下需要使用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.

Donald E. Knuth写了一篇关于这个主题的经典论文,它与Dijkstra的论文相匹配,那就是结构化编程与语句。阅读既有助于重建上下文,也有助于对主题的非教条性理解。在本文中,Dijkstra对这个案例的观点被报道,并且更加强烈:

Donald E. Knuth:我相信通过呈现这样一个 事实上,我并不是不同意 Dijkstra的观点,因为 他最近写道: “请不要落入。 相信我是可怕的 教条的关于[去]的教条的 声明)。我觉得很不舒服 感觉别人在做 宗教出来了,好像 编程的概念问题 只用一个小技巧就能解决,用什么 一种简单的编码纪律!”


当前回答

跳跃的例子在Java字符串类源代码:

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 ...]

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

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

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

没有激起宗教战争的意图。

其他回答

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

Goto可以很好地用于状态机。循环中的switch语句(按典型重要性排序):(A)不能实际代表控制流,(b)丑陋,(c)可能效率低下,这取决于语言和编译器。因此,您最终为每个状态编写一个函数,并执行类似“return NEXT_STATE;”的操作,这甚至看起来像goto。

当然,以易于理解的方式对状态机进行编码是很困难的。然而,这些困难都与使用goto无关,也不能通过使用替代控制结构来减少。除非你的语言有一个“状态机”结构。我不喜欢。

在极少数情况下,当您的算法通过通过有限的允许转换集(goto)连接的节点(状态)序列的路径而不是通过任何更具体的控制流(循环、条件等等)来理解时,那么应该在代码中显式地说明这一点。你应该画一个漂亮的图。

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

Setjmp /longjmp比goto“更危险”,因为它们更难正确使用,更不用说理解了。

从来没有,将来也不会有 永远是,任何语言都是 写不好一点也难 代码。——唐纳德·克努特

从C中去掉goto并不会使用C编写好的代码变得更容易。事实上,这样做反而会忽略一点,即C应该能够作为一种被美化的汇编语言。

接下来是“有害的指针”,然后是“有害的鸭子类型”。那么,当他们来拿走你不安全的编程结构时,谁来保护你呢?是吗?

如果你用C写一个VM,使用(gcc的)计算gotos是这样的:

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是不好的。它会使代码很难遵循,也会使维护变得困难。以《糟糕的过去》中的一些Basic程序为例。

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

也就是说,对于某些特殊的目的来说,goto是非常有用的(我发现自己对那些没有goto的语言非常恼火)。我主要在C语言中使用它来打破多层循环,或者进行错误处理;我相信c#的语言特性意味着您不必这样做。(它在生成自动生成代码时也非常有用,但大多数人在现实生活中不会遇到这种情况。)

goto还有一个纯粹的政治问题:很多人讨厌它,在代码中使用它,即使是合理的,也可能会引起问题。如果这是作业代码,那么是的,重写它,否则你可能会被扣分。否则,我倾向于把它留在那里,直到下次你需要对那部分进行维护。

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

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

以下陈述是概括;尽管抗辩例外总是可能的,但通常(以我的经验和拙见)不值得冒险。

Unconstrained use of memory addresses (either GOTO or raw pointers) provides too many opportunities to make easily avoidable mistakes. The more ways there are to arrive at a particular "location" in the code, the less confident one can be about what the state of the system is at that point. (See below.) Structured programming IMHO is less about "avoiding GOTOs" and more about making the structure of the code match the structure of the data. For example, a repeating data structure (e.g. array, sequential file, etc.) is naturally processed by a repeated unit of code. Having built-in structures (e.g. while, for, until, for-each, etc.) allows the programmer to avoid the tedium of repeating the same cliched code patterns. Even if GOTO is low-level implementation detail (not always the case!) it's below the level that the programmer should be thinking. How many programmers balance their personal checkbooks in raw binary? How many programmers worry about which sector on the disk contains a particular record, instead of just providing a key to a database engine (and how many ways could things go wrong if we really wrote all of our programs in terms of physical disk sectors)?

以上附注:

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

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

在代码中的“do something”点,我们可以高度自信地声明a大于b。(是的,我忽略了未捕获整数溢出的可能性。我们不要拘泥于一个简单的例子。)

另一方面,如果代码是这样读的:

    ...
    goto 10
    ...
    a = b + 1
    10: /* do something with a */
    ...
    goto 10
    ...

标记10的方法的多样性意味着我们必须更加努力才能确信a和b在这一点上的关系。(事实上,在一般情况下,这是不可判断的!)

关于第4点,代码中“去某个地方”的整个概念只是一个比喻。除了电子和光子(用于余热),CPU内部没有任何东西真正“去”到任何地方。有时候,我们会放弃一个比喻,转而使用另一个更有用的比喻。我记得(几十年前!)遇到过一种语言

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

通过将action-1和action-2编译为行外无参数例程,然后使用单个双参数VM操作码(使用条件的布尔值来调用其中一个)在虚拟机上实现。这个概念只是“选择现在调用什么”,而不是“去这里或去那里”。再一次,换一个比喻。