GOTO仍然被认为有害吗?[关闭]


281

每个人都知道Dijkstra写给编辑信:转到被认为是有害的声明(也在此处是 .html成绩单和此处 .pdf),并且自那时以来一直在大力推动以尽可能避免使用goto声明。尽管可以使用goto生成无法维护的庞大代码,但仍保留在现代编程语言中。甚至Scheme中的高级连续控制结构也可以描述为复杂的goto。

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

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


迪克斯特拉本人对此感到遗憾,对此他不负责。在EWD1308的末尾(也在此处 .pdf),他写道:

最后是一个短故事,供记录。1968年,ACM通讯发布了我的文本,标题为“ goto声明被认为是有害的 ”,在后来的几年中,我的文本经常被引用,但是遗憾的是,经常看到这些文本的作者对此一无所知。标题,通过成为模板,成为了我成名的基石:我们几乎可以在X上看到标题为“ X认为有害”的各种文章,其中包括标题为“ Dijkstra认为有害”的文章。但是发生了什么事?我已经提交了标题为“ 反对goto陈述的案例 ”的论文为了加快出版速度,编辑将其更改为“编辑的信”,并在此过程中为其赋予了自己的发明的新标题!编辑是Niklaus Wirth。

与Dijkstra相匹配的,经过深思熟虑的经典论文是Donald E. Knuth撰写的结构化编程与go to语句。阅读既有助于重新建立上下文,也有助于对主题的非教条式理解。在本文中,Dijkstra对于此案的观点得到了报道,并且更加有力:

唐纳德·E·努斯(Donald E. Knuth):我相信提出这种观点实际上并不与迪杰斯特拉的想法完全不同意,因为他最近写了以下文章:“请不要陷入相信我对[去发言。我有不舒服的感觉,其他人正在一个宗教出来,仿佛编程的概念问题可以通过一个单一的技巧来解决,通过编码规则的简单形式!


1
C#的goto与Dijkstra所说的goto不同,正是出于Dijkstra所说的原因。那个 goto既有害,又没有那么多必要,因为现代语言提供了替代的控制结构。C#goto非常受限制。
jalf

25
当Gotos增加清晰度时,它们是很好的。如果您有一个较长的嵌套循环,那么最好退出循环,而不是设置“ break”变量并中断直到退出。
simendsjo 2010年

28
如果您在4个深度上有一个嵌套循环(不是一件好事),要想突破所有循环,就需要设置临时值。对我而言,这里的goto更加清晰了,IDE应该可以轻松显示goto的位置。就是说,goto的使用应该是稀疏的,而且我认为只能向下移动以跳过代码
simendsjo 2010年

7
我建议您阅读标记为九千零一的线程goto
Matti Virkkunen 2010年

5
显然有比使用更糟的事情goto:一起入侵结构化的编程工具来实现一个goto

Answers:


184

以下陈述是概括;尽管总是可以提出例外请求,但通常(根据我的经验和拙见)不值得冒险。

  1. 不受限制地使用内存地址(GOTO或原始指针)提供了太多机会来避免容易避免的错误。
  2. 在代码中到达特定“位置”的方法越多,对该点系统状态的信心就越差。(见下文。)
  3. 结构化编程恕我直言不是要“避免GOTO”,而是要使代码的结构与数据的结构匹配。例如,重复的数据结构(例如数组,顺序文件等)自然由重复的代码单元处理。具有内置的结构(例如,每时,每当,直到等),使程序员可以避免重复相同的代码模式的乏味。
  4. 即使GOTO是底层实现细节(并非总是如此!)仍低于程序员应该考虑的水平。有多少程序员在原始二进制文件中平衡他们的个人支票簿?有多少程序员担心磁盘上的哪个扇区包含特定的记录,而不仅仅是提供数据库引擎的密钥(如果我们真的按照物理磁盘扇区来编写所有程序,那么会出错多少种方法)?

上面的脚注:

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

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

在代码中的“执行某些操作”时,我们可以高置信度声明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操作码,该操作码使用条件的布尔值来调用一个或另一个。这个概念只是“选择现在要调用的内容”,而不是“去这里或去那里”。同样,只是一个隐喻的改变。


2
好点。在高级语言中,goto甚至没有任何意义(考虑在Java中的方法之间跳转)。Haskell函数可以包含一个表达式。尝试通过goto跳出来!
机械蜗牛

2
后记的工作方式类似于您的第4点示例。
luser droog 2012年

如果说“类似”,Smalltalk的工作原理与第4点示例类似,那么它的意思是“没有像程序语言一样”。:P语言没有流程控制;所有决策都是通过多态性处理的(true并且false属于不同类型),并且if / else的每个分支基本上都是lambda。
cHao 2014年

这些都是有效的要点,但最后它们只是重申“如果滥用” goto有多糟糕。中断,继续,退出,返回,gosub,settimeout,全局,包含等都是需要在脑海中追踪事物流的现代技术,并且都可能被误用于创建意大利面条代码并跳​​过以创建可变状态的不确定性。公平地说,尽管我从未亲眼看到过goto的错误用法,但我也只见过使用过一两次。这表明,总是有更好的东西要使用。
Beejor 2015年

goto使用现代编程语言(Go)stackoverflow.com/a/11065563/3309046
nishanths

245

XKCD的GOTO漫画

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

我以为这部漫画很好地说明了“我可以重组程序的流程,或者改用一个小“ GOTO”。” 当您的设计较弱时,GOTO是较弱的出路。 迅猛龙捕食弱者


41
GOTO可以从一个任意点“跳”到另一个任意点。迅猛龙从无处跳到这里!
rpattabi 2010年

29
我觉得这个笑话一点都不有趣,因为每个人都知道您还必须先链接才能执行。
Kinjal Dixit 2010年

31
您的同事是错误的,并且显然没有阅读Knuth的论文。
吉姆·巴尔特

27
多年以来,我一直都在为避免使用goto而混淆代码,否则会扭曲代码。@jimmcKeeth,上面的xkcd卡通无法确定goto是弱的。它是在逗弄使用它的歇斯底里。

4
您确实意识到Delphi库源代码包含goto语句。这些是goto的适当用法。
David Heffernan

131

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

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

return;

err_cleanup:
...

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


29
我同意,在合法的用例中,goto可以简化代码并使其更具可读性/可维护性,但是似乎有些goto-phobia
随处可见

48
@Bob:如果要清除局部变量,则很难将err_cleanup代码移到子例程中。
Niki

4
实际上,我在COM / VB6中使用它只是因为没有其他选择,而不是因为它是替代选择。如今,我对try / catch / finally感到多么高兴。
Rui Craveiro 09年

10
@ user4891惯用的C ++方法不是尝试{} catch(){cleanup; },而是RAII,需要清理的资源是在析构函数中完成的。每个构造函数/析构函数仅管理一个资源。
大卫·斯通

5
不使用goto编写C的方法有两种:而且两者都短得多。要么:if(f())if(g())if(h())返回成功;清理(​​); 返回失败;或:if(f()&& g()&& h())返回成功;清理(​​); 返回失败;
LHP 2014年

124

我只能回想一次goto。我有一系列五个嵌套的计数循环,并且我需要能够根据特定条件从内部从整个结构中突围:

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

ENDOFLOOPS:

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

没有迅猛龙袭击我。


83
“将其重构为函数,并用return :)替换goto”,区别是?真的有什么区别?还不去吗?Return也像goto一样阻止结构化流程,在这种情况下,它们以相同的方式进行操作(即使goto可以用于
卑鄙的

38
嵌套很多循环通常是代码散发出的全部气味。除非像5维数组乘法那样做,否则很难想象其中某些内部循环无法有效地提取到较小函数中的情况。像所有经验法则一样,我想有一些例外。
道格·麦克林

5
仅当使用支持退货的语言时,才用退货代替它。
罗伦·佩希特尔

31
@leppie:goto出于同样的原因,反叛并为我们提供结构化编程的这一代人也拒绝了早期的回报。取决于代码的可读性,表达程序员意图的清晰程度。创建功能的唯一目的就是避免使用恶意的关键字,这会导致不良的内聚性:治愈比疾病更糟糕。
2012年

21
@ButtleButkus:坦白说,这同样糟糕,甚至还不至于更糟。至少goto可以使用来明确指定目标。使用break 5;,(1)我必须计算循环闭合次数才能找到目的地;(2)如果循环结构曾经改变,则很可能需要更改该数字以保持目标正确。如果我要避免goto,那么收益应该在于不必手动跟踪类似的东西。
cHao

92

我们已经进行了讨论,我支持我的观点

此外,我已经厌倦了人们将高级语言结构描述为“ goto变相”,因为他们显然一点都不了解。例如:

甚至Scheme中的高级连续控制结构也可以描述为复杂的goto。

那是完全废话。每个控制结构都可以根据实现,goto但是这种观察是完全琐碎且无用的。goto不会因为具有积极影响而认为是有害的,而是因为具有负面影响,而结构化编程已消除了这些负面影响。

同样,说“ GOTO是一种工具,并且作为所有工具,它都可以使用和滥用”完全是不可能的。没有现代的建筑工人会使用岩石来宣称它是“工具”。岩石已被锤子取代。goto已由控制结构取代。如果建筑工人在没有锤子的情况下被困在野外,他当然会改用岩石。如果程序员必须使用不具有功能X的劣等编程语言,那么,当然,她可能必须改goto而使用。但是,如果她在其他任何地方使用它而不是适当的语言功能,则她显然无法正确理解该语言,并且使用错误。就这么简单。


82
当然,正确使用岩石并不是锤子。它的正确用途之一是打磨石头,或用于磨削其他工具。如果使用得当,即使是低矮的岩石也是很好的工具。您只需要找到正确的用法。goto也是如此。
Kibbee

10
那么Goto的正确用法是什么?对于每种可以想象的情况,都有另一个更合适的工具。即使你的磨石实际上是由高科技手段时下更换,即使他们仍然做岩石。原材料和工具之间存在很大差异。
康拉德·鲁道夫

8
@jalf:Goto无疑在C#中确实存在。见stackoverflow.com/questions/359436/…–
乔恩·斯基特

51
许多人赞成这个职位,我感到非常沮丧。您的帖子看起来似乎很有效,因为您从未想过您实际上在执行什么逻辑,因此您没有注意到自己的谬误。请允许我解释一下您的整个帖子:“在每种情况下都有一个用于goto的高级工具,因此永远不要使用gotos。” 这是合乎逻辑的双重条件,因此,您的整个帖子本质上都是在问一个问题:“您怎么知道在每种情况下都存在用于goto的高级工具?”
编码样式为2010年

10
@Coding:不,您完全错过了帖子的要旨。这是一个还击,而不是一个孤立的,完整的说法。我只是指出了主要论点“ for”的谬误goto。到目前为止,您是对的,因为我没有提出反对(goto本质上是无意的)论据,因此没有任何疑问。
Konrad Rudolph'2

91

仅出于此目的,Goto在我要包含在程序中的内容列表中的位置非常低。这并不意味着它是不可接受的。

Goto对于状态机可能很好。循环中的switch语句(按典型重要性顺序)是:(a)实际上并不代表控制流,(b)难看,(c)取决于语言和编译器的潜在效率低下。因此,您最终会为每个状态编写一个函数,并执行诸如“ return NEXT_STATE;”之类的操作。甚至看起来像goto。

当然,很难以易于理解的方式对状态机进行编码。但是,该困难与使用goto无关,并且可以通过使用其他控制结构来减少它。除非您的语言具有“状态机”构造。我的不是。

在极少数情况下,您的算法实际上是通过一系列节点(状态)的路径最可理解的,该路径由一组有限的允许的过渡(元)连接,而不是通过任何更特定的控制流(循环,条件, ),那么在代码中应该是明确的。您应该画一个漂亮的图。

setjmp / longjmp可以很好地实现异常或类似异常的行为。尽管没有得到普遍的称赞,但异常通常被认为是“有效”的控制结构。

setjmp / longjmp比goto“更危险”,因为它们很难正确使用,不要介意。

从来没有,也永远不会有任何一种语言,很难编写出不好的代码。-唐纳德​​·努斯(Donald Knuth)。

将goto移出C并不会使用C编写好的代码变得更加容易。实际上,它宁愿错过C 应该能够充当美化汇编语言的观点。

接下来将是“指针被认为有害”,然后是“鸭子输入被认为有害”。那么当他们拿走您不安全的编程结构时,谁将为您辩护???


14
就我个人而言,是我本应接受的评论。我想向读者指出的一件事是,深奥的“状态机”一词包括诸如词法分析器之类的日常事物。检查lex somtime的输出。充满了gotos。
TED

2
您可以在循环(或事件处理程序)中使用switch语句来做状态机。我已经完成了许多状态机,而不必使用jmp或goto。
Scott Whitlock,2009年

11
+1状态机上的那些箭头比其他任何控制结构都更紧密地映射到“ goto”。当然,您可以在循环内使用开关-就像可以使用一堆的goto而不是花一会儿来解决其他问题一样,但这通常是一个主意。这就是整个讨论的重点。
Edmund

2
我可以在最后一段引用你吗?
克里斯·卢茨

2
而且,在2013年,我们已经进入(并且已经过去)“指针被认为有害”阶段。
Isiah Meadows 2014年

68

Linux中:在内核陷阱上使用goto在内核代码中,与Linus Torvalds和一个“新手”讨论了在Linux代码中使用GOTO的问题。那里有一些很好的观点,莱纳斯穿着那种通常的傲慢态度:)

一些段落:

莱纳斯:“不,您被CS人士洗脑了,他们以为Niklaus Wirth实际上知道他在说什么。他没有。他没有僵硬的线索。”

--

莱纳斯:“我认为goto很好,它们通常比大量缩进更具可读性。”

--

Linus:“当然,在像Pascal这样的愚蠢的语言中,标签无法描述,而goto的标签可能会很糟糕。”


9
这是一个好点吗?他们正在用一种别无选择的语言讨论其使用。在汇编中编程时,所有分支和跳转都是 goto的。C过去曾经是“便携式汇编语言”。此外,通道引述你说什么关于为什么他认为goto语句是好的。
jalf

5
哇。令人失望的阅读。您可能会认为,像Linus Torvalds这样的大型OS家伙会比这样说更了解。Pascal(旧式的pascal,而不是现代的Object版本)是Mac OS Classic在68k年代编写的,并且是当时最先进的操作系统。
梅森惠勒

6
@mason Classic Mac OS具有一些Pascal库(最终-Pascal运行时在早期的Mac中占用了太多内存),但是大多数核心代码是用Assembler编写的,尤其是图形和UI例程。
吉姆·多维

30
Linus仅争辩(就像在该讨论中像Rik van Riel一样)使用goto来处理退出状态,并且他这样做的依据是,如果使用C的替代结构会带来复杂性。
查尔斯·斯图尔特

21
恕我直言,Linus在这个问题上是正确的。他的观点是,使用C语言编写的,需要实现类似于异常处理的东西的内核代码,最清楚,最简单地使用goto编写。成语goto cleanup_and_exit是后藤的几个“好”的应用领域之一,现在离开了我们forwhileif管理我们的控制流。另请参阅:programmers.stackexchange.com/a/154980
steveha,2013年

50

在C语言中,goto仅在当前函数的范围内起作用,这倾向于将任何潜在的错误本地化。setjmp而且longjmp更加危险,因为它们是非本地的,复杂的并且依赖于实现。但是,在实践中,它们太晦涩难懂且不常见,不会引起很多问题。

我相信gotoC语言的危险被大大夸大了。请记住,原始goto争论发生在老式的BASIC之类的语言时代,在那儿初学者会像这样编写意大利面条代码:

3420 IF A > 2 THEN GOTO 1430

Linus在此描述了对以下内容的适当使用gotohttp : //www.kernel.org/doc/Documentation/CodingStyle(第7章)。


7
当BASIC首次面世时,除了GOTO nnnn和GOSUB mmmm之外,别无选择。之后添加结构化构建体。
乔纳森·勒夫勒

7
您错过了要点……即使那样您也不必写意大利面条……您的GOTO总是可以
有条不紊

还值得注意的是,仅当setjmp/ 的行为longjmp被用作从同一范围内的其他位置跳到该范围内某个位置的手段时,才指定/ 的行为。一旦控制离开作用域setjmp的执行范围,longjmp对由创建的结构使用的任何尝试setjmp都将导致未定义的行为。
2012年

9
某些版本的BASIC可以帮助您GOTO A * 40 + B * 200 + 30。不难看出这是多么方便和非常危险。
乔恩·汉纳

1
@Hjulle将计算表达式,然后转到具有该数字的代码行(显式行号是大多数早期方言的要求)。ZX Spectrum Basic是接受这一点的人
乔恩·汉纳

48

如今,很难看到有关该GOTO声明的重大内容,因为“结构化编程”人员大都赢得了辩论,并且当今的语言具有足够避免的控制流结构GOTO

计算goto现代C程序中s 的数量。现在添加的数量breakcontinuereturn语句。此外,添加的使用次数ifelsewhileswitchcase。那就是GOTO如果您在1968年Dijkstra写信的时候用FORTRAN或BASIC编写程序,程序将有多少个程序。

当时的编程语言缺乏控制流程。例如,在原始的Dartmouth 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
    
  • 即使您的IF语句不需要ELSE,它仍然限于一行,通常由组成GOTO

  • 没有DO...LOOP声明。对于非FOR循环,您必须以显式结束循环GOTOIF...GOTO从头开始。

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

所以,你结束了一个很大GOTO程序中的秒。而且您不能依赖于GOTOs在单个子例程中的限制(因为GOSUB...RETURN子例程的概念太弱了),因此这些GOTOs可以随处可见。显然,这使得控制流程难以遵循。

这就是反GOTO运动的来历。


要注意的另一件事是,如果一个循环中有一些代码而很少执行,那么编写代码的首选方式是420 if (rare_condition) then 3000// 430 and onward: rest of loop and other main-line code// 3000 [code for rare condition]// 3230 goto 430。以这种方式编写代码可以避免在常见的主线情况下发生任何分支或跳转,但这确实使事情难以理解。如果某些分支限制为例如+/- 128字节,并且有时没有互补对(例如,存在“ cjne”但不存在“ cje”),则汇编代码中避免分支的情况可能更糟。
2012年

我曾经为8x51编写过一些代码,该代码有一个中断,每128个周期运行一次。在该ISR的常见情况下,每花费一个额外的周期,就会使主线代码的执行速度降低1%以上(我认为主线通常可以使用128个周期中的90个周期),并且任何分支指令都需要两个周期(在分支和失败情况下)。该代码有两个比较-一个通常报告相等。另外,不相等。在这两种情况下,极少数情况的代码都超过128个字节。所以...
超级猫

cjne r0,expected_value,first_comp_springboard/...../ cjne r1,unexpected_value,second_comp_fallthrough//`AJMP second_comp_target` // first_comp_springboard: ajmp first_comp_target// second_comp_fallthrough: ...。这不是一个很好的编码模式,但是当每个周期都很重要时,就会执行此类操作。当然,在1960年代,这种优化水平比今天更为重要,尤其是由于现代CPU经常需要进行怪异的优化,并且即时编译系统可能能够为当CPU所不存在的CPU应用优化代码。有问题的代码被编写。
supercat

34

在某些情况下,转到可以为“实际”异常处理提供某种替代。考虑:

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


1
尽管我倾向于同意您在这里所说的话,但是这些break语句位于控件结构内部这一事实可以清楚地说明它们的作用。对于该goto示例,阅读代码的人员必须查看代码以找到标签,从理论上讲,标签实际上可能早于控件结构。我对老式C的经验不足,无法判断一个绝对优于另一个,但是这两种方法都需要权衡取舍。
维维安河

5
@DanielAllenLangdon:breaks在循环内的事实清楚地表明它们已退出循环。那不是“他们到底在做什么”,因为实际上,根本没有循环!其中没有什么可以重复的,但是直到最后还不清楚。你有一个“循环”的事实从未运行不止一次,这意味着控制结构被滥用。通过goto示例,程序员可以说goto error_handler;。它更明确,甚至更难遵循。(Ctrl + F,“ error_handler:”以找到目标。尝试使用“}”来实现。)
cHao 2013年

1
我曾经在空中交通管制系统中看到过与您的第二个示例相似的代码-因为“ goto不在我们的词典中”。
QED 2013年

30

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

本文介绍了Dijkstra的建议,并说明了这种情况的有效情况。但是他还给出了许多反例(问题和算法),这些反例仅使用结构化循环就不容易重现。

本文包含问题的完整说明,历史记录,示例和反例。


25

Goto认为有帮助。

我从1975年开始编程。到1970年代的程序员,“转到认为有害”一词或多或少地表明具有现代控制结构的新编程语言值得尝试。我们确实尝试了新的语言。我们迅速转换。我们再也回不去了。

我们从不回头,但是,如果您还年轻,那么您从来没有去过那里。

现在,古代的编程语言背景可能并不能很好地发挥作用,只是可以指示程序员的年龄。尽管如此,年轻的程序员缺乏这种背景,所以他们不再理解“ goto认为有害”的口号在推出之初传达给其目标受众的信息。

一个不明白的口号不是很有启发。最好忘记这样的口号。这样的口号无济于事。

然而,这个特别的口号“ Goto被认为是有害的”已经走上了不死之路。

goto不会被滥用吗?答:可以,但是那又如何呢?实际上,每个编程元素都可能被滥用。bool例如,卑鄙的人比我们中某些人所想相信的更多地被滥用。

相比之下,我不记得自1990年以来遇到过一次实际的goto滥用事件。

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

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


24

由杰伊·巴拉(Jay Ballou)添加一个答案吸引,我将添加我的£0.02。如果Bruno Ranschaert尚未这样做,我会提到Knuth的“使用GOTO语句进行结构化编程”文章。

我未曾讨论过的一件事是在Fortran教科书中教授的那种虽然不是很普遍的代码。诸如扩展DO循环范围和开放式编码子例程之类的事情(请记住,这应该是Fortran II,Fortran IV或Fortran 66,而不是Fortran 77或90)。至少有语法细节不正确的机会,但是概念应该足够准确。每种情况下的代码片段都在单个函数内。

请注意,由Kernighan&Plauger 撰写的优秀但过时(且已绝版)的书《The Programming of Style,第二版》(The Elements of Programming Style,2nd Edn)包括了一些真实的例子,这些例子滥用了GOTO时代(70年代后期)的编程教科书。但是,下面的材料并非来自该书。

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。Googling 分配的goto告诉我,它已从Fortran 95中删除。对结构化编程革命进行了深入讨论,可以说,这是公开的,始于Dijkstra的“ GOTO被认为有害”字母或文章。

如果不了解在Fortran(以及其他语言,其中大多数已经正确地抛弃了其他语言)中所做的各种事情,我们新手很难理解Dijkstra正在处理的问题的范围。哎呀,直到那封信出版十年后,我才开始编程(但是我确实不幸有一阵子无法在Fortran IV中编程)。


2
如果要查看使用goto“野外使用” 的某些代码的示例,请参阅问题:工作的Bose-Hibbard排序算法,其中显示了1963年发布的一些(Algol 60)代码。原始布局与现代编码标准不符。澄清(缩进)的代码仍然相当难以理解。goto那里的陈述确实使(非常)难以理解算法的作用。
乔纳森·莱夫勒

1
由于年龄太小,无法体验到打孔卡附近的所有内容,因此阅读有关重新打孔的问题很有启发。+1
Qix-蒙尼卡(Monica)

20

不会有GOTO认为有害的事情。

GOTO是一种工具,并且作为所有工具,都可以使用和滥用

有,但是,在编程世界许多工具有一种倾向,被滥用不是被更多的使用,和GOTO就是其中之一。Delphi 的WITH语句是另一个。

就我个人而言,我都不在典型代码中使用任何代码,但是我对GOTOWITH的使用都感到奇怪,这是有根据的,另外一种解决方案将包含更多的代码。

最好的解决方案是让编译器仅警告您该关键字已污染,并且您必须在该语句周围填充一些编译指示以摆脱警告。

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


GOTO的问题在于,它打破了我们认为现代编程语言所认为的重要不变量。举一个例子,如果我调用一个函数,我们假设当函数完成时,它将以正常方式或通过异常堆栈展开将控制权返回给调用者。如果该功能滥用了GOTO,那么当然不再适用。这使得我们的代码很难推理。仅仅避免滥用GOTO是远远不够的。如果我们的依赖关系滥用了GOTO,仍然会出现问题...
Jonathan Hartley

...因此,为了知道我们是否可以对函数调用进行推理,我们需要检查每个传递依赖项的源代码的每一行,并检查它们是否滥用GOTO。因此,即使我们自己完美地使用(或从不使用它),仅GOTO语言的存在也破坏了我们自信地推理自己的代码的能力。因此,GOTO不仅仅是要谨慎使用的工具。它被系统地破坏了,单方面认为它在某种语言中的存在是有害的。
乔纳森·哈特利

即使goto从C#中清除了关键字,IL中仍然存在“在此处跳转”的概念。没有goto关键字,可以轻松构造无限循环。如果您认为这种无法保证被调用的代码将返回的意思是“无法推理该代码”,那么我想说我们从来没有这种能力。
Lasse V. Karlsen

啊,您很有见识,找到了我们沟通不畅的根源。goto从单词的原始意义上讲,C#中的in并不是真正的“ goto”。它是一个较弱的版本,仅允许在函数内跳转。我使用“ goto”的感觉允许在过程中的任何地方跳转。因此,尽管C#具有“ goto”关键字,但可以说它从来没有真正的goto。是的,真正的goto在IL级别可用,就像将任何一种语言编译成汇编语言一样。但是程序员对此有所保护,在正常情况下无法使用它,因此它不算在内。
乔纳森·哈特利

顺便说一句,我在这里描述的观点并非最初是我的,而是Dijkstra 1967年的原始论文的核心,我认为他的编辑将其重新命名为“ Goto认为有害”,这已成为50年代经常被引用的模因正是因为它如此具有革命性,洞察力和被普遍接受而多年。
乔纳森·哈特利


17

自从我开始在linux内核中做一些事情以来,gotos并没有像以前那样困扰我。起初,我很震惊地看到他们(内核人员)在我的代码中添加了goto。从那以后,我已经习惯了在某些有限的上下文中使用gotos,现在偶尔会自己使用它们。通常,这是跳转到函数末尾的某种goto,以执行某种清理和纾困,而不是在函数中的多个位置复制相同的清理和纾困。通常,它还不足以传递给另一个函数-例如,释放一些本地(k)malloc分配的变量是一种典型情况。

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

setjmp / longjmp有他们的地方-但是这个地方是您不太可能会拜访的地方,但很长一段时间后就会出现。

编辑:我只是看了代码。我实际上使用的是siglongjmp(),而不是longjmp(这没什么大不了的,但是我忘记了siglongjmp甚至存在。)


15

如果您使用C语言编写VM,事实证明,使用(gcc)计算的goto是这样的:

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

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


唯一的问题,这不是标准,是吗?
2011年

&&op_inc当然不会编译,因为(左)&期望一个左值,但是(右)却&产生一个右值。
fredoverflow

7
@FredO:这是一个特殊的GCC运算符。但是,除了极少数情况外,我都会拒绝此代码,因为我最确定地无法理解wtf的情况。
Puppy

尽管这是一个示例(最大速度优化),可以证明goto和隐秘代码的合理使用,但仍应给予高度评价,以解释对优化的需求,大致的工作原理以及最佳的逐行代码可以发表评论。因此,它仍然会失败,但是因为它使维护程序员陷入困境。但是我确实喜欢VM示例。谢谢。
MicroservicesOnDDD

14

因为goto可用于混淆元编程

Goto既是高级别低级别的控制表达,并且作为结果,它只是不具有适合于大多数问题适当的设计图案。

从某种意义上说,goto是一种底层操作,它实现了诸如while或之类的高级操作,这是低级的foreach

从某种意义上说,它是高级的,当以某种方式使用时,它会以清晰的顺序,以不间断的方式执行代码(结构化循环除外),并将其更改为足够多goto的逻辑即可抓取的逻辑部分-可以动态重新组合的逻辑包。

所以,有一个平淡无奇的邪恶的一面goto

平淡的侧面是一个向上的goto可以实现一个完全合理的循环和向下的跳转可以做一个完全合理的breakreturn。当然,实际的whilebreakreturn会更具可读性,因为穷人不必goto为了获得大局而模拟的效果。因此,这是一个坏主意。

邪恶的一面涉及不使用常规的同时,打破或返回跳转,但使用它的什么所谓的意大利面条逻辑。在这种情况下,对goto满意的开发人员正在用goto的迷宫构建代码段,而唯一的理解方法是从整体上进行模拟,这在有许多goto的情况下是一件非常累人的任务。我的意思是,想象一下评估代码时遇到的麻烦,其中else并非与恰好相反,在某些情况if下嵌套ifs可能允许被外部if等拒绝的嵌套等等。

最后,要真正涵盖该主题,我们应该注意,除Algol之外,基本上所有早期语言最初仅根据其版本对单个语句进行声明if-then-else。因此,执行条件块的唯一方法是goto使用逆条件块围绕它。我知道是疯了,但是我已经读了一些旧规范。请记住,第一批计算机是用二进制机器代码编程的,所以我认为任何一种HLL都是救生员。我想他们对所获得的HLL功能到底是不是太挑剔。

说完所有我曾经坚持的goto所有程序之后,我写了“只是为了惹恼纯粹主义者”


4
+1惹恼了纯粹主义者!:-)
Lumi

10

对程序员拒绝使用GOTO语句就像告诉木匠不要使用锤子,因为锤子在钉子时会损坏墙壁。真正的程序员知道如何以及何时使用GOTO。我遵循了其中一些所谓的“结构化程序”,为避免使用GOTO,我看到了此类Horrid代码,以便可以射击程序员。好的,为了反过来,我也一遍又一遍地看到了一些真正的意大利面条代码,那些程序员也应该被枪杀。

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

  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

3
CRT'这是正确的吗?(Y / N):':输入纱线直到纱线='Y'或纱线='N'; 等
joel.neely

5
确实,但更重要的是,真正的程序员知道什么时候使用goto-,并且知道为什么。避免使用忌讳的语言构造,因为$ programming_guru这么说,这就是崇尚货物编程的定义。
Piskvor在

1
这是一个很好的类比。为了打钉而又不损坏基材,请不要消除锤子。而是使用一种称为钉子冲床的简单工具。这是具有锥形末端的金属销,该金属销的尖端具有空心凹痕,可与钉头牢固连接(这些工具的尺寸不同,适用于不同的钉子)。打钉机的另一钝端用锤子敲打。
哈兹2015年


7

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


6

关于唯一的地方,我同意后藤可能被使用是当你需要处理错误,出现错误的每个特定点需要特殊处理。

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

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

始终可以在没有goto的情况下正确执行此操作,但是在这种情况下,Goto实际上是主要针对可读性和可维护性的更好解决方案。

-亚当


5

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

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


5
您认为究竟由谁创建编译器?
tloach

10
编译器,当然!
Seiti

3
我认为他的意思是“ GOTO是只应由编译器发出的代码使用的东西”。
西蒙·布坎

这是反对的情况goto。在可能用goto手工编写的状态机实现枚举器的地方,我们可以yield改用。
乔恩·汉纳

使用默认大小写打开多个字符串(以防止编译为if-else)以使用goto语句进行切换。
M.kazem Akhgary,2015年

5

直到C和C ++(包括其他原因)标记为中断并继续运行,goto才会继续发挥作用。


那么标记为break或继续的地方与goto有何不同?
Matthew Whited

3
他们不允许在控制流中进行完全任意的跳跃。
DrPizza

5

如果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跳入块的情况下完成吗?不。


1
出于恐惧而避免使用语言功能是大脑受损的迹象。这也比地狱还好。
矢量Gorgoth 2014年

4

我避免使用它,因为同事/经理无疑会在代码审查中或当他们偶然发现它时质疑它的使用。虽然我认为它有用途(例如,错误处理案例),但您会与其他开发人员发生冲突,而他们将对此产生某种类型的问题。

这不值得。


关于C#中Try ... Catch块的好处是,当异常冒泡到异常处理程序时,它们负责清理堆栈和其他分配的资源(称为展开堆栈)。这使Try ... Catch比Goto更好,因此,如果您有Try ... Catch,请使用它。
Scott Whitlock,2009年

我有一个更好的解决方案:将您要弹出的代码块包装在'do {...} while(0);'中。环。这样,您可以以与goto相同的方式跳出,而不会产生try / catch循环的开销(我不了解C#,但是在C ++中try的成本低,catch的成本高,所以看起来引发一个简单跳转就足够的异常)。
Jim Dovey

10
吉姆(Jim),问题在于,这无非是愚蠢的round回方式获得goto。
编码样式为2010年

4

我实际上发现自己被迫使用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是合理的。

雨果


非GOTO方式将是使用fast_calculations函数,这会产生一些开销。在大多数情况下,可能不会引起注意,但是正如您所说的,这对速度至关重要。
凯尔·克罗宁

1
嗯,这不足为奇。具有绝对疯狂的性能准则的所有代码都将几乎破坏所有最佳实践。最佳实践是为了达到可访问性和可维护性,而不是挤出更多的10毫秒或节省5字节的内存。
乔纳森(Jonathon)

1
@ JonathonWisnoski,goto的合法使用也消除了笨拙的意大利面条式代码,这些代码与老鼠的变量嵌套打扰,以跟踪我们的前进方向。
vonbrand,2015年

4

在几乎所有可以使用goto的情况下,您都可以使用其他构造来执行相同的操作。无论如何,编译器都会使用Goto。

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


4

从这里的任何答案中我都没有看到的一件事是,“ goto”解决方案通常比经常提到的结构化编程解决方案之一更有效

考虑多嵌套循环的情况,在这种情况下,使用“ goto”而不是一堆if(breakVariable)节显然更有效。解决方案“将循环放入函数并使用返回值”通常是完全不合理的。在循环使用局部变量的情况下,您现在必须将它们全部传递给函数参数,从而可能处理由此引起的额外麻烦。

现在考虑清理案例,我经常使用它,它很常见,以至于负责了try {} catch {}结构,这在许多语言中都没有。完成同一件事所需的检查和额外变量的数量远比进行跳转的一条或两条指令差,而且,附加功能解决方案根本不是解决方案。您不能告诉我,这更易于管理或更具可读性。

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

“ goto有害”的说法对结构化编程非常有帮助,即使它总是过于笼统。在这一点上,我们都已经听到了足够多的声音,以至于警惕使用它(我们应该如此)。当它显然是这项工作的正确工具时,我们就不必为此感到恐惧。


3

您可以使用它来打破深层嵌套的循环,但是大多数情况下,无需深层嵌套的循环,就可以将代码重构为更简洁的代码。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.