每个人都知道Dijkstra 给编辑的信件:转到被认为有害的声明(也就是这里 .html脚本和这里的 .pdf)并且从那时起就有一个强大的推动,尽可能避开goto声明.虽然可以使用goto来生成不可维护的,庞大的代码,但它仍然保留在现代编程语言中.甚至Scheme中的高级连续控制结构也可以被描述为复杂的goto.
什么情况下可以使用goto?什么时候最好避免?
作为后续问题:C提供了一对函数setjmp和longjmp,它们不仅可以在当前堆栈帧内,而且可以在任何调用帧中进行转换.这些应该被视为像goto一样危险吗?更危险吗?
Dijkstra本人对这个头衔表示遗憾,他对此不负责任.在EWD1308(也是这里 .pdf)结束时,他写道:
最后是一个短篇小说的记录.1968年,ACM的通讯以" 被认为有害的goto声明 "的标题发表了我的一篇文章,遗憾的是,在后来的几年中,这一期刊最常被引用,但是,经常被作者看到的不多于标题,通过成为一个模板成为我的名声的基石:我们会看到几乎任何X的标题"X被认为有害"的各种文章,包括一个标题为"Dijkstra被认为有害"的文章.但是发生了什么?我提交了一份题为" 反对goto声明的案件 "的论文"为了加快出版速度,编辑已经变成了一封"给编辑的信",并在此过程中给了他一个新的自己的发明名称!编辑是Niklaus Wirth.
关于这个主题的经过深思熟虑的经典论文,与Dijkstra的相关,是结构化编程,由Donald E. Knuth撰写.阅读都有助于重新建立背景和对主题的非教条性理解.在本文中,Dijkstra对此案的观点得到了报道,甚至更为强烈:
Donald E. Knuth:我相信通过提出这样一种观点,我实际上并不同意Dijkstra的观点,因为他最近写了以下内容:"请不要陷入相信我非常悲惨的陷阱.去发言.我有其他人正在一个宗教出来的不舒服的感觉,就好像编程的概念问题可以通过一个单一的技巧来解决,通过编码规则的简单形式! "
Jim McKeeth.. 240
我的一位同事说,使用GOTO的唯一原因是如果你把自己编程到一个角落,这是唯一的出路.换句话说,提前进行适当的设计,以后您不需要使用GOTO.
我认为这部漫画很精彩地说明了"我可以重组程序的流程,或者使用一点'GOTO'代替." 当你的设计薄弱时,GOTO是一种微弱的出路. 迅猛龙捕食弱者.
我的一位同事说,使用GOTO的唯一原因是如果你把自己编程到一个角落,这是唯一的出路.换句话说,提前进行适当的设计,以后您不需要使用GOTO.
我认为这部漫画很精彩地说明了"我可以重组程序的流程,或者使用一点'GOTO'代替." 当你的设计薄弱时,GOTO是一种微弱的出路. 迅猛龙捕食弱者.
以下陈述是概括; 虽然总是可以恳求例外,但通常(根据我的经验和拙见)并不值得冒这个风险.
无限制地使用内存地址(GOTO或原始指针)提供了太多机会来轻松避免错误.
到达代码中特定"位置"的方式越多,对该系统状态的信心就越不自信.(见下文.)
结构化编程IMHO不是关于"避免GOTO",而是关于使代码结构与数据结构相匹配.例如,重复数据结构(例如,阵列,顺序文件等)由重复的代码单元自然地处理.具有内置结构(例如,while,for,until,for-each等)允许程序员避免重复相同的陈词滥调代码模式的乏味.
即使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编译为out-of-line无参数例程,然后使用单个双参数VM操作码,使用条件的布尔值来调用其中一个,从而在虚拟机上实现.这个概念只是"选择现在要调用的东西",而不是"去这里或去那里".再一次,只是一个隐喻的变化.
有时在单个函数中使用GOTO替代异常处理是有效的:
if (f() == false) goto err_cleanup; if (g() == false) goto err_cleanup; if (h() == false) goto err_cleanup; return; err_cleanup: ...
COM代码似乎经常陷入这种模式.
我只记得使用过一次goto.我有一系列五个嵌套计数循环,我需要能够根据某些条件从内部早期打破整个结构:
for{ for{ for{ for{ for{ if(stuff){ GOTO ENDOFLOOPS; } } } } } } ENDOFLOOPS:
我可以轻松地声明一个布尔中断变量并将其用作每个循环的条件的一部分,但在这个实例中我决定GOTO同样实用且同样可读.
没有迅猛龙攻击我.
我们已经讨论了这个问题,我坚持自己的观点.
此外,我讨厌大家介绍"作为更高级别的语言结构goto
的化身",因为他们显然还没有得到一点在所有.例如:
甚至Scheme中的高级连续控制结构也可以被描述为复杂的goto.
这完全是胡说八道.每个控制结构都可以实现,goto
但这种观察是完全无关紧要的.goto
由于其积极影响而被认为是有害的,但由于其负面影响,这些已被结构化编程所消除.
同样地,说"GOTO是一种工具,并且作为所有工具,它可以被使用和滥用"完全不合适.没有现代建筑工人会使用岩石并声称它"是一种工具."岩石已被锤子取代.goto
已被控制结构所取代.如果建筑工人在没有锤子的情况下被困在野外,当然他会使用岩石代替.如果程序员必须使用没有特征X的劣质编程语言,那么当然她可能不得不使用goto
.但如果她在其他任何地方使用它而不是相应的语言功能,她显然不能正确理解语言并错误地使用它.它真的很简单.
Goto在我的列表中非常低,只是为了它而包含在程序中.这并不意味着它是不可接受的.
Goto可以用于状态机.循环中的switch语句(按典型重要性顺序):( a)实际上不代表控制流,(b)丑陋,(c)取决于语言和编译器,可能效率低下.所以你最终会为每个状态编写一个函数,并执行"return NEXT_STATE;"之类的操作.甚至看起来像goto.
当然,很难以一种易于理解的方式对状态机进行编码.然而,使用goto没有任何困难,并且通过使用替代控制结构不能减少任何困难.除非您的语言具有"状态机"构造.我没有.
在极少数情况下,您的算法在通过一组有限的允许转换(gotos)连接的节点(状态)的路径中最容易理解,而不是通过任何更具体的控制流(循环,条件,诸如此类) ),然后在代码中应该是明确的.你应该绘制一个漂亮的图表.
setjmp/longjmp可以很好地实现异常或类似异常的行为.虽然没有得到普遍赞扬,但例外通常被认为是"有效"的控制结构.
setjmp/longjmp比goto"更危险",因为它们更难以正确使用,从不介意理解.
从来没有,也没有任何语言可以编写糟糕的代码. - 唐纳德克努特
从C中取出goto不会让在C中编写好的代码变得更容易.实际上,它宁愿错过C 应该能够作为一种美化的汇编语言.
接下来它将是"被认为有害的指针",然后"鸭子打字被认为是有害的".然后,当他们拿走你不安全的编程结构时,谁会留下来为你辩护?嗯?
在Linux中:在内核陷阱中使用goto In Kernel Code,与Linus Torvalds和一个关于在Linux代码中使用GOTO的"新人"进行了讨论.那里有一些非常好的点,而Linus穿着那种平常的傲慢:)
一些段落:
Linus:"不,你被CS人员洗脑了,他们认为Niklaus Wirth实际上知道他在说什么.他没有.他没有一个fr c c的线索."
-
Linus:"我认为goto很好,而且它们通常比大量缩进更具可读性."
-
Linus:"当然,在像Pascal这样的愚蠢语言中,标签无法描述,goto可能会很糟糕."
在C中,goto
仅在当前函数的范围内工作,这往往会定位任何潜在的错误.setjmp
并且longjmp
更加危险,非本地化,复杂化和依赖于实现.然而,在实践中,它们太模糊,并且不常见导致许多问题.
我相信goto
C 中的危险被夸大了.请记住,最初的goto
论点发生在像老式BASIC这样的语言时代,初学者会像这样编写意大利面条代码:
3420 IF A > 2 THEN GOTO 1430
这里Linus描述了一个适当的用法goto
:http://www.kernel.org/doc/Documentation/CodingStyle(第7章).
今天,很难看到关于GOTO
声明的大问题,因为"结构化编程"人们大多赢得了辩论,今天的语言有足够的控制流结构来避免GOTO
.
计算goto
现代C程序中的s 数.现在添加的数量break
,continue
和return
语句.此外,添加的使用次数if
,else
,while
,switch
或case
.这就是GOTO
当你在Dijkstra写下他的信时,如果你在1968年写FORTRAN或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
循环,您必须以显式GOTO
或IF...GOTO
回到开头结束循环.
没有SELECT CASE
.你必须使用ON...GOTO
.
所以,你结束了一个很大的GOTO
程序中的秒.并且你不能依赖于GOTO
s在单个子例程中的限制(因为GOSUB...RETURN
子例程的概念很弱),所以这些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而采取了一些非常愚蠢的东西,那就不要了.
Donald E. Knuth在1992年的CSLI"Literate Programming"一书中回答了这个问题.在p.17有一篇文章" 带有goto语句的结构化编程 "(PDF).我认为这篇文章也可能已在其他书籍中发表过.
这篇文章描述了Dijkstra的建议,并描述了这种情况的有效性.但他也提供了许多反例(问题和算法),这些例子只能使用结构化循环来轻松复制.
本文包含问题的完整描述,历史,示例和反例.
被Jay Ballou吸引并添加一个答案,我将加上0.02英镑.如果Bruno Ranschaert还没有这样做,我会提到Knuth的"使用GOTO语句进行结构化编程"一文.
我没有看过的一件事就是在Fortran教科书中讲授的一些代码,虽然不常见,但却很常见.像DO循环和开放编码子程序的扩展范围(记住,这将是Fortran II,Fortran IV或Fortran 66 - 而不是Fortran 77或90).语法细节至少有可能是不精确的,但概念应该足够准确.每种情况下的片段都在一个函数内.
请注意,Kernighan&Plauger 撰写的优秀但过时(绝版)的书" The Elements of Programming Style,2nd Edn "包含了一些现实生活中的GOTO滥用例子,这些例子来自其时代(70年代后期)的编程教科书.但是,下面的材料不是那本书.
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.谷歌搜索分配goto告诉我它已从Fortran 95中删除.用白色结构编程革命,可以说是公开的Dijkstra的"GOTO Considered Harmful"字母或文章.
如果不了解Fortran所做的各种事情(以及其他语言,其中大多数已被正确地撇开),我们新手很难理解Dijkstra正在处理的问题的范围.哎呀,直到那封信发表十年后我才开始编程(但我确实不幸在Fortran IV中编程了一段时间).
转到认为有帮助.
我在1975年开始编程.对于20世纪70年代的程序员来说,"转向被认为是有害的"这些词或多或少地表示具有现代控制结构的新编程语言值得尝试.我们确实尝试过新语言.我们很快转换了.我们再也没有回去过.
我们再也没有回去,但是,如果你年轻,那么你从来没有去过那里.
现在,除了作为程序员年龄的指标之外,古代编程语言的背景可能不是很有用.尽管如此,年轻的程序员缺乏这种背景,因此他们不再理解在引入时向其目标受众传达的" 转向被认为有害"的口号.
一个人不理解的标语并不是很有启发性.最好忘记这样的口号.这样的口号没有帮助.
然而,这个特殊的口号"Goto被认为是有害的"已经成为了自己的不死生命.
可以转到不被滥用?答:当然可以,但那又怎样?实际上每个编程元素都可能被滥用.bool
例如,谦卑的人比我们想要相信的人更容易受到虐待.
相比之下,我不记得自1990年以来遇到一个单一的,实际的goto滥用实例.
goto最大的问题可能不是技术问题,而是社交问题.有时候不太了解的程序员似乎觉得弃用goto会让他们听起来很聪明.您可能不得不满足这些程序员.这就是人生.
关于goto今天最糟糕的事情是它使用不够.
没有GOTO认为有害的东西.
GOTO是一种工具,作为所有工具,它可以被使用和滥用.
然而,编程世界中有许多工具比使用更容易被滥用,而GOTO就是其中之一.Delphi 的WITH语句是另一个.
就个人而言,我不会在典型代码中使用任何一种,但我已经保证了GOTO和WITH的奇怪用法,并且替代解决方案将包含更多代码.
最好的解决方案是编译器只是警告你关键字被污染了,你必须在语句周围填写一些pragma指令来摆脱警告.
这就像告诉你的孩子不要用剪刀跑.剪刀也不错,但使用它们可能不是保持健康的最佳方法.
自从我开始在linux内核中做了一些事情以来,getos并没有像以前那样困扰我.起初我有点害怕看到他们(内核人员)在我的代码中添加了getos.我已经习惯于在某些有限的环境中使用gotos,现在偶尔也会使用它们.通常,它是一个转到函数末尾进行某种清理和纾困的goto,而不是在函数中的几个位置复制相同的清理和挽救.通常情况下,它不足以传递给另一个函数 - 例如,释放一些局部(k)malloc变量是典型的情况.
我编写的代码只使用了setjmp/longjmp一次.它是在MIDI鼓音序器程序中.回放发生在与所有用户交互不同的进程中,回放过程使用共享内存和UI进程来获取回放所需的有限信息.当用户想要停止播放时,播放过程只是做了一个"回到开头"的longjmp重新开始,而不是在用户希望它停止时执行的任何地方的一些复杂的展开.它工作得很好,很简单,在这种情况下我从来没有遇到任何与之相关的问题或错误.
setjmp/longjmp有他们的位置 - 但是那个地方是你不可能访问的地方,但很长一段时间.
编辑:我只看了代码.它实际上是我使用的siglongjmp(),而不是longjmp(不是说这是一个大问题,但我忘记了siglongjmp甚至存在.)
它永远不会,只要你能够为自己思考.
如果你用C编写VM,事实证明使用(gcc)计算得到的结果是这样的:
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是一种原始操作,可以实现更高while
或更高的foreach
东西.
从某种意义上讲它是高级别的,它以某种方式使用代码以一种清晰的顺序执行,以不间断的方式执行,除了结构化循环,它将它变成逻辑片段,有足够的goto
s,抓取 - 动态重组的逻辑包.
所以,有一个平淡而邪恶的一面goto
.
在平淡的侧面是一个向上的goto可以实现一个完全合理的循环和向下指向转到可以做一个完全合理的break
或return
.当然,实际的while
,break
或者return
更具可读性,因为穷人不需要模拟效果,goto
以获得全局.所以,一般来说一个坏主意.
在邪恶的一面涉及不使用常规的同时,打破或返回跳转,但使用它的什么所谓的意大利面条逻辑.在这种情况下,goto-happy开发人员正在从goto的迷宫中构建代码片段,理解它的唯一方法是在整体上模拟它,当有很多goto时,这是一个非常累人的任务.我的意思是,想象一下评估代码的麻烦,其中的代码else
不完全是反转的if
,其中嵌套的if
s可能允许某些被外部拒绝的东西if
等.
最后,为了真正涵盖这一主题,我们应该注意到,除了Algol之外,基本上所有早期语言最初只根据其版本的单一语句if-then-else
.因此,执行条件块的唯一方法是goto
使用反条件围绕它.疯了,我知道,但我读过一些陈旧的规格.请记住,第一台计算机是用二进制机器代码编程的,所以我认为任何一种HLL都是救星; 我猜他们对于HLL的具体功能并不太挑剔.
说过我曾经把所有的东西都粘贴goto
到我编写的每个程序中"只是为了惹恼纯粹主义者".
拒绝将GOTO声明用于程序员就像告诉木匠不要使用锤子一样,因为它可能会在锤击钉子时损坏墙壁.一个真正的程序员知道如何以及何时使用GOTO.我跟在其中一些所谓的"结构化程序"后面我看到这样的Horrid代码只是为了避免使用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
"在此链接http://kerneltrap.org/node/553/2131 "
具有讽刺意味的是,消除goto引入了一个错误:省略了spinlock调用.
原始论文应该被认为是"无条件的GOTO被认为是有害的".它特别提倡一种基于条件(if
)和迭代(while
)构造的编程形式,而不是早期代码常见的测试和跳转.goto
在某些语言或情况下仍然有用,因为没有适当的控制结构.
关于我同意Goto 可以使用的唯一地方是当你需要处理错误时,每个特定点发生错误都需要特殊处理.
例如,如果您正在抓取资源并使用信号量或互斥量,则必须按顺序抓取它们,并且应始终以相反的方式释放它们.
有些代码需要非常奇怪的模式来获取这些资源,并且您不能只编写一个易于维护和理解的控制结构来正确处理这些资源的获取和释放以避免死锁.
总是可以在没有goto的情况下正确地完成它,但在这种情况下和其他几个Goto实际上是更好的解决方案,主要是为了可读性和可维护性.
-亚当
一个现代的GOTO用法是由C#编译器为yield yield定义的枚举数创建状态机.
GOTO应该由编译器而不是程序员使用.
直到C和C++(以及其他罪魁祸首)标记了中断并继续,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跳入块的情况下完成吗?不。