GOTO仍然被认为有害?

每个人都知道Dijkstra的给编辑的信:转到被认为有害的声明(还有在这里 .html和在这里 .pdf),从那时起就有一种强大的推动力,尽可能地避免goto语句。虽然可以使用goto来生成不可维护的、庞大的代码,但它仍然保留在现代编程语言中。即使Scheme中先进的延续控制结构也可以被描述为一个复杂的goto。

在什么情况下需要使用goto?什么时候最好避免?

作为一个后续问题:C提供了一对函数setjmp()和longjmp(),它们不仅提供了在当前堆栈帧内进行跳转的能力,还提供了在任何调用帧内进行跳转的能力。这些应该被认为和goto一样危险吗?更危险?


Dijkstra自己也对这个头衔感到后悔,因为这不是他的责任。在EWD1308(也是在这里 .pdf)的结尾,他写道:

最后讲一个小故事。 1968年,ACM通讯 发表了我的一篇文章 title "考虑的goto语句 有害的" 最常被引用的, 然而,遗憾的是,往往是作者 谁只看到过它 标题,这成为基石 我的名声成为了一个模板:我们 能看到下面各种各样的文章吗 标题“X被认为有害”;为 几乎任何X,包括一个名为 “Dijkstra认为是有害的”。但 发生了什么事?我提交了一份 这篇论文的标题是“强有力的” goto语句" 为了加快其出版速度, 编辑已经改成了“写给” “编辑”,在这个过程中 给了他自己一个新头衔 发明!编辑是尼克劳斯 Wirth . < / p >

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

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

94353 次浏览

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

您可以使用它来打破深度嵌套循环,但大多数情况下,无需深度嵌套循环,代码可以重构得更干净。

没有GOTO被认为有害这样的东西。

GOTO是一个工具,和所有工具一样,它可以被使用和滥用

然而,在编程世界中有许多工具倾向于滥用而不是使用, GOTO就是其中之一。Delphi的语句是另一个。

就我个人而言,我不使用任何在典型代码中,但我有转到的奇怪用法,这是有保证的,另一种解决方案将包含更多的代码。

最好的解决方案是编译器只警告你关键字是受污染的,你必须在语句周围塞几个pragma指令来消除警告。

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

有时候,在一个函数中使用GOTO作为异常处理的替代是有效的:

    if (f() == false) goto err_cleanup;
if (g() == false) goto err_cleanup;
if (h() == false) goto err_cleanup;
    

return;
    

err_cleanup:
...

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

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

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

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

我认为这是有害的,因为大多数初学者都失去了桌子锯和goto的手指。

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

goto的基本思想是给你太多的自由去做你不想做的事情。它可能在与goto语句无关的地方导致错误,因此使代码维护更加困难。如果你认为你需要一个goto语句,你错了:),你应该重新思考你的代码结构。这就是为什么现代编程语言投入了大量精力来提供可读的、可维护的流控制结构和异常处理机制。

我也不同意拉塞夫克的观点。由于goto被滥用的次数多于正确使用的次数,我相信它在设计良好的语言中没有一席之地。即使对于goto的“理想”用途,其他需要更多代码的方式也应该是首选。

所以总的来说,是的,它仍然被认为是有害的。

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

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

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

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

亚当

我避免使用它,因为同事/经理无疑会在代码评审或偶然发现它时质疑它的使用。虽然我认为它有一些用途(例如错误处理案例),但你会遇到一些其他开发人员,他们会遇到一些类型的问题。

这不值得。

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

Linux:在内核代码中使用goto on Kernel Trap中,有一个与Linus Torvalds和一个“新人”关于在Linux代码中使用GOTOs的讨论。有一些非常好的观点,莱纳斯穿着平常的傲慢:)

一些段落:

莱纳斯:“不,你被洗脑了 国安局的人认为尼克劳斯 Wirth其实知道自己是什么 说什么。他没有。他不 给我个线索。" < / p >

-

莱纳斯:“我觉得goto很好,而且 它们通常比 大量的压痕。" < / p >

-

莱纳斯:“当然,用愚蠢的语言 就像帕斯卡,在那里标签不能 描述性的,后去可能是不好的。" < / p >

Donald E. Knuth在1992年CSLI出版的《识字编程》一书中回答了这个问题。在第17页有一篇文章“使用goto语句进行结构化编程”(PDF)。我认为这篇文章也可能发表在其他书中。

本文描述了Dijkstra的建议,并描述了这种建议有效的情况。但他也给出了一些反例(问题和算法),这些反例仅用结构化循环是无法轻易重现的。

这篇文章包含了对问题、历史、例子和反例的完整描述。

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

    for{
for{
for{
for{
for{
if(stuff){
GOTO ENDOFLOOPS;
}
}
}
}
}
}
    

ENDOFLOOPS:

我可以很容易地声明一个布尔中断变量,并将其用作每个循环的条件的一部分,但在这种情况下,我认为GOTO是一样实用和一样可读的。

没有迅猛龙攻击我。

我们已经有了这个讨论,我支持我的观点

此外,我受够了人们将高级语言结构描述为“伪装的goto”,因为他们显然没有理解在所有。例如:

即使Scheme中先进的连续控制结构也可以被描述为复杂的后向。

那完全是胡说八道。每一个控制结构可以根据goto来实现,但这个观察结果是完全琐碎和无用的。goto被认为是有害的,因为它的积极影响,而是因为它的消极后果,这些已经被结构化编程消除了。

同样,说“GOTO是一种工具,和所有工具一样,它可以被使用和滥用”是完全离题的。没有哪个现代建筑工人会在使用石头时声称它是“工具”。石头已被锤子所取代。goto已被控制结构所取代。如果一个建筑工人被困在野外,没有锤子,他当然会用石头来代替。如果一个程序员不得不使用一种没有特性X的低级编程语言,那么,当然她可能不得不使用goto来代替。但如果她在其他地方使用了它,而不是适当的语言功能,她显然没有正确理解语言,使用错误。其实就是这么简单。

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

在C语言中,goto只在当前函数的作用域内工作,这往往会本地化任何潜在的错误。setjmplongjmp更加危险,它们是非本地的、复杂的和依赖于实现的。然而,在实践中,它们太模糊和不常见,不会引起很多问题。

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

3420 IF A > 2 THEN GOTO 1430

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

如果你用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;
}

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

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

  1. 不受约束地使用内存地址(GOTO或原始指针)提供了太多的机会来犯容易避免的错误。
  2. 到达一个特定“地点”的方法越多;在代码中,对系统在这一点上的状态越不自信。(见下文)。
  3. 结构化编程在我看来不是“避免goto”;以及如何使代码结构与数据结构相匹配。例如,重复的数据结构(例如数组、顺序文件等)自然由重复的代码单元处理。拥有内置结构(例如while, for, until, for-each等)可以让程序员避免重复相同的老套代码模式的乏味。
  4. 即使GOTO是低层次的实现细节(并不总是这样!),它也低于程序员应该考虑的层次。有多少程序员用原始二进制来平衡他们的个人支票簿?有多少程序员担心磁盘上的哪个扇区包含一个特定的记录,而不仅仅是提供一个数据库引擎的键(如果我们真的根据物理磁盘扇区编写所有程序,会有多少种情况出错)?

以上附注:

关于第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的方法多种多样,这意味着我们必须更加努力地工作,才能确信在这一点上ab之间的关系。(事实上,在一般情况下,这是不可判断的!)

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

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

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

使用goto使得编写“意大利面条式的代码”变得非常容易,而这并不是特别容易维护的。要遵循的最重要的规则是编写可读的代码,当然这取决于项目的目标是什么。作为“最佳实践”,避免goto是一个好主意。极端编程类型将其称为“代码气味”,因为它表明您可能正在做错误的事情。在循环中使用break与goto非常相似,只是它不是goto,但这再次表明代码可能不是最优的。这就是为什么,我相信,不要发现更多的现代编程漏洞也是很重要的,这些漏洞本质上是一个不同的名字。

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

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

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

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

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

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

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

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

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

在我所见过的每个平台上,高级控制结构都被实现为低级的goto(跳转)。例如,Java虚拟机有一个跳转字节码,但是没有if、else、while、For等字节码。

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

回答你的问题,去走仍然被那些认为它有害的人认为是有害的。Goto很容易失去结构化编程的优点。

最后,这是你的程序;所以你的决定。我建议在您能够自己回答问题之前不要使用goto,而是在特定问题的上下文中使用。

如果GOTO本身是邪恶的,那么编译器也是邪恶的,因为它们生成jmp。如果跳转到代码块,特别是跟随一个指针,在本质上是邪恶的,那么RETurn指令就是邪恶的。相反,邪恶在于滥用的可能性。

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

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(++)中处理它的方式是定义一些宏:

#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)上面的结构化状态机转换为结构化代码:

{
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,游戏还能完成吗?不。

在我编程生涯的早期,有一次,我编写了一个由一系列链函数组成的程序,其中每个函数在给定成功条件和完成条件的情况下调用它的后继函数。

这是一个丑陋的笨拙,有许多严重的问题,最严重的是,在它下面的所有函数都终止之前,任何函数都不能终止。

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

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

拒绝程序员使用GOTO语句就像告诉木匠不要使用锤子,因为锤子在钉钉子的时候可能会损坏墙壁。真正的程序员知道如何以及何时使用GOTO。我跟随了一些所谓的“结构化程序”,我看到过这样可怕的代码,只是为了避免使用GOTO,我可以射杀程序员。好吧,为另一方辩护,我也见过一些真正的意大利面条代码,同样,那些程序员也应该被枪毙。

下面是我找到的一个小代码示例。

  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

----------------------- 或 ----------------------

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

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

我有一个复杂的对象,我需要对它做一些操作。如果对象处于一种状态,那么我就可以进行快速操作,否则我就必须进行慢速操作。问题是,在某些情况下,在缓慢的手术过程中,可能会意识到这可以用快速手术来完成。

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在这里是合理的。

雨果

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

具有讽刺意味的是,取消goto引入了一个错误:自旋锁调用被省略了。

被Jay Ballou添加的答案所吸引,我会加入0.02英镑。如果Bruno Ranschaert还没有这样做,我就会提到Knuth的“结构化编程与GOTO语句”。篇文章。

有一件事我没有看到讨论,那就是那种在Fortran教科书中教过的代码,尽管它并不常见。例如DO循环的扩展范围和开放代码子程序(记住,这将是Fortran II, Fortran IV或Fortran 66 -而不是Fortran 77或90)。至少有可能语法细节不准确,但概念应该足够准确。每种情况下的代码片段都在单个函数中。

请注意,由Kernighan &撰写的优秀但过时(绝版)的书“编程风格的元素,第2版”;Plauger从那个时代(70年代末)的编程教科书中列举了一些滥用GOTO的真实例子。然而,下面的材料并不是来自那本书。

DO循环的扩展范围

       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通常都是大写的。

中非子例程

       ...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。谷歌分配转到告诉我它是从Fortran 95中删除的。值得注意的是,结构化编程革命可以说是从Dijkstra的“GOTO被认为是有害的”开始的。信件或文章

如果不了解Fortran语言(以及其他语言,其中大多数已经半途而用了)中所做的事情,我们这些新手很难理解Dijkstra所处理的问题的范围。见鬼,直到那封信发表10年后,我才开始编程(但我确实不幸地在Fortran IV中编程了一段时间)。

虽然我认为在任何情况下都最好避免“goto”,但也有例外。 例如,我所见过的一个地方,与其他更复杂的方法相比,goto语句是优雅的解决方案,那就是为解释器实现尾部调用消除。< / p >

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

从来都不是,只要你能独立思考。

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

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而付出了荒谬的代价:

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而求助于一些非常愚蠢的事情,那么就不要这样做。

用于调度的计算gotos通常比非常大的switch语句更容易理解。

对于错误和联合线程,我认为setcontext或setjmp(如果可用)是“更好的”。

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

自从我开始在linux内核中做一些事情以来,gotos不再像以前那样困扰我了。一开始,当我看到他们(内核人员)在我的代码中添加gotos时,我有点害怕。从那以后,我已经习惯了在一些有限的环境中使用goto,现在我自己也会偶尔使用它们。通常,它是跳转到函数末尾执行某种清理和纾困的goto,而不是在函数的多个地方重复相同的清理和纾困。通常,它不是一个大到可以交给另一个函数的东西——例如释放一些局部(k)malloc'ed变量是一个典型的例子。

我只编写了一次使用setjmp/longjmp的代码。这是MIDI鼓的音序器程序。回放发生在一个独立于所有用户交互的进程中,并且回放进程使用与UI进程共享的内存来获得执行回放所需的有限信息。当用户想要停止回放时,回放进程只是做了一个longjmp“回到开始”重新开始,而不是在用户想要停止它时恰好正在执行的任何地方进行复杂的展开。它运行得很好,很简单,在这种情况下我从未遇到任何与它相关的问题或错误。

Setjmp /longjmp有它们自己的位置——但那个位置是你不太可能只在很长一段时间内访问一次的地方。

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

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

setjmp/longjmp构造在某些情况下(缺少“真正的”异常时,或者在实现Chicken scheme之类的东西时)可能有用,但在“普通”编程中没有它的位置。

在一个完美的世界里,我们永远不需要GOTO。然而,我们生活在一个不完美的世界。我们并没有包含我们所能想到的所有控制结构的编译器。有时我觉得使用GOTO比拼凑一个并不存在的控制结构更好。

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

loop:
GetSomeData;
if GotData then
Begin
ProcessTheData;
StoreTheResult;
Goto Loop;
End;

对我来说,这比

Repeat
GetSomeData;
Flag := GotData;
if Flag then
Begin
ProcessTheData;
StoreTheResult;
End;
Until Not Flag;

有些时候

Function GotTheData;


Begin
GetSomeData;
Result := GotData;
End;


While GotTheData do
Begin
ProcessTheData;
StoreTheResult;
End;

不是一个可行的答案,我坚信代码应该是清晰的。如果我必须做一个注释来解释代码在做什么,我会考虑是否可以让代码更清晰,并去掉注释。

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

这是GOTO有时比过去更安全的一个例子。

在某些情况下,GOTO的使用与提前从函数返回相同(即提前跳出循环)。然而,良好的形式是有争议的。

因为goto可以用于令人困惑的元编程

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

在某种意义上,它是低级,因为goto是一个基本操作,实现了更高级的操作,比如whileforeach之类的。

它是高层,在某种意义上,当以某种方式使用时,它会以一种不间断的方式,以一种清晰的顺序执行的代码(除了结构化循环之外),并将其转换为逻辑片段,这些逻辑片段如果有足够的__abc0,则是一个动态重新组装的逻辑大包。

因此,goto平淡无奇的邪恶的两个边。

平淡的一面是一个向上指向的goto可以实现一个完全合理的循环,而一个向下指向的goto可以执行一个完全合理的breakreturn。当然,一个实际的whilebreak,或return将是更可读的,因为可怜的人不需要模拟goto的效果来获得大的图片。总的来说,这是个坏主意。

邪恶的一面包含一个不使用goto for while、break或return的例程,而是用于所谓的意大利面条的逻辑。在这种情况下,喜欢goto的开发人员正在从goto的迷宫中构建代码片段,而理解它的唯一方法是在精神上将其作为一个整体来模拟,当有许多goto时,这是一项非常累人的任务。我的意思是,想象一下计算代码的麻烦,其中else不是if的逆,嵌套的if可能允许一些被外部if拒绝的东西,等等。

最后,为了真正涵盖这个主题,我们应该注意到,基本上所有早期语言(除了Algol)最初都只根据它们的if-then-else版本创建单个语句。因此,创建条件块的唯一方法是使用逆条件将其括起来。我知道这很疯狂,但我看过一些旧的说明书。请记住,第一台计算机是用二进制机器码编程的,所以我认为任何一种HLL都是救命稻草;我猜他们对他们得到的HLL功能并不太挑剔。

说了这么多,我曾经把一个goto插入到我写的每个程序“只是为了激怒纯粹主义者”中。

今天,很难看出GOTO语句的重大意义,因为“结构化编程”的人基本上赢得了这场辩论,而且今天的语言有足够的控制流结构来避免GOTO

计算现代C程序中__abc的数量。现在添加breakcontinuereturn语句的数量。此外,加上你使用ifelsewhileswitchcase的次数。这大约是1968年Dijkstra写这封信时,如果你用FORTRAN或BASIC语言编写程序,你的程序会有多少__abc9。

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

  • IF语句没有ELSE。如果你想要一个,你必须这样写:

    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
    
  • Even if your IF statement didn't need an ELSE, it was still limited to a single line, which usually consisted of a GOTO.

  • There was no DO...LOOP statement. For non-FOR loops, you had to end the loop with an explicit GOTO or IF...GOTO back to the beginning.

  • There was no SELECT CASE. You had to use ON...GOTO.

So, you ended up with a lot of GOTOs in your program. And you couldn't depend on the restriction of GOTOs to within a single subroutine (because GOSUB...RETURN was such a weak concept of subroutines), so these GOTOs could go anywhere. Obviously, this made control flow hard to follow.

This is where the anti-GOTO movement came from.

在我看来,“goto有害”更多的是关于状态的封装和一致性。

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

“goto有害”的问题是,它让只看机制规则而不理解的程序员认为唯一可用的流控制是返回方法,这很容易导致通过引用传递大量状态,而又导致缺乏状态封装,这正是“goto有害”试图摆脱的东西。

遵循典型的“OO”代码库中的控制流,并告诉我我们仍然没有意大利面条代码....(顺便说一下,我并不是指那些经常让人讨厌的“馄饨”代码——馄饨代码的执行路径通常是非常简单的,即使对象关系不是立即明显的)。

或者,换一种说法,避免gotos而将所有东西都作为子例程,只有在每个子例程只修改局部状态时才有用,只有通过该子例程(或至少该对象)才能修改局部状态。

几乎所有可以使用goto的情况,都可以使用其他结构实现相同的效果。无论如何,编译器都会使用Goto。

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

goto有一些问题。一是很难看到代码是如何流动的。因为花括号,所以更容易看到if-block,但是goto隐藏了它。此外,while和if本质上也是goto,但它们有助于解释为什么要在代码中来回跳转。有了一个固定的目标,你必须自己拼凑起来。

作为练习,试着编写一些计算斐波那契数列的代码,看看当你完成后,它有多难阅读。

如果你要处理这些代码,那么我建议你写一些单元测试,然后重写它。否则,就顺其自然吧。

尽管如此,有时出于性能原因,使用goto“可能”是合适的。

并不是说去做本身不好;而是当同样的逻辑可以用另一种方式更清楚地表达时,使用goto是不好的。它会使代码很难遵循,也会使维护变得困难。以《糟糕的过去》中的一些Basic程序为例。

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

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

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

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

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

C语言没有这个特性。但您仍然经常需要在函数开始时获取资源,并在结束时释放它们。

你的函数可能有一个或多个错误条件导致它提前返回。您不希望重复资源释放代码。解决方案是使用goto。

例子:

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;
}

我从任何的答案中没有看到的一件事是,“去”解决方案通常是更高效的,而不是经常提到的结构化编程解决方案之一。

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

现在考虑一下清理情况,我自己经常使用这种情况,而且这种情况非常常见,可能是try{} catch{}结构在许多语言中都不可用的原因。完成相同任务所需的检查和额外变量的数量远比完成跳转所需的一两个指令要糟糕得多,而且,额外的函数解决方案根本不是解决方案。你不能告诉我这更容易管理或更易于阅读。

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

“goto有害”这句话对迈向结构化编程非常有帮助,即使它总是一种过度概括。在这一点上,我们都听到了足够多的使用它的谨慎(我们应该)。当它显然是工作的正确工具时,我们不需要害怕它。

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可以很有用,这里是一个用c++编写的非常强大的开源象棋引擎stockfish的例子goto只是跳过了一些条件检查(效率增益),如果没有goto语句,程序就必须这样做。如果goto语句标签位于goto声明之后,那么它们是相当无害且可读的。

后藤认为他很有帮助。

我从1975年开始编程。对于20世纪70年代的程序员来说,“goto被认为是有害的”这个词或多或少地表明,具有现代控制结构的新编程语言值得一试。我们确实尝试了新的语言。我们很快就皈依了。我们再也没有回去过。

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

现在,拥有古老编程语言的背景可能并没有太大用处,除非它能显示程序员的年龄。尽管如此,年轻的程序员缺乏这方面的背景,所以他们不再理解口号“goto被认为有害”所传达的信息

一个人不理解的口号是不太有启发性的。也许最好忘记这些口号。这样的口号无济于事。

然而,这个特别的口号,“后藤有害”,已经有了它自己的不死生命。

能去不被虐待吗?回答:当然,但那又怎样?实际上每一个编程元素可以都被滥用了。例如,简陋的bool被滥用的次数比我们中的一些人愿意相信的要多。

相比之下,自1990年以来,我不记得遇到过一次真正的goto滥用案例。

goto最大的问题可能不是技术问题,而是社交问题。不太懂的程序员有时似乎觉得不赞成goto会让他们听起来很聪明。您可能需要不时地满足这样的程序员。生活就是这样。

今天goto最糟糕的事情是它没有被充分使用。