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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了GOTO仍被视为有害吗? [关闭]相关的知识,希望对你有一定的参考价值。

每个人都知道Dijkstra的Letters to the editor: go to statement considered harmful(也是here .html transcript和here .pdf)并且从那时起就有一个强大的推动力,只要有可能就避开goto语句。虽然可以使用goto来生成不可维护的庞大代码,但它仍然存在于modern programming languages中。甚至Scheme中的高级continuation控制结构也可以被描述为复杂的goto。

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

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


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

最后是一个短篇小说的记录。 1968年,ACM的通讯以“被认为有害的goto声明”的标题发表了我的一篇文章,遗憾的是,在后来的几年里,这一期刊最常被引用,但是,经常被作者看到的不多于标题,通过成为一个模板成为我的名声的基石:我们会看到几乎任何X的标题“X被认为有害”的各种文章,包括一个标题为“Dijkstra被认为有害”的文章。但是发生了什么?我提交了一篇题为“反对goto声明的案例”的论文,为了加快其出版速度,编辑已经变成了“致编辑的信”,并在此过程中给了它一个新的他自己发明的头衔!编辑是Niklaus Wirth。

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

Donald E. Knuth:我相信通过提出这样一种观点,我实际上并不同意Dijkstra的观点,因为他最近写了以下内容:“请不要陷入相信我非常悲惨的陷阱。我有一种令人不舒服的感觉,就是其他人正在创造一种宗教信仰,好像编程的概念问题可以通过一个简单的编码规则来解决!

答案

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

  1. 无限制地使用内存地址(GOTO或原始指针)提供了太多机会来轻松避免错误。
  2. 到达代码中特定“位置”的方式越多,对该系统状态的信心就越不自信。 (见下文。)
  3. 结构化编程IMHO不是关于“避免GOTO”,而是关于使代码结构与数据结构相匹配。例如,重复数据结构(例如,阵列,顺序文件等)由重复的代码单元自然地处理。具有内置结构(例如,while,for,until,for-each等)允许程序员避免重复相同的陈词滥调代码模式的乏味。
  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的多种方式意味着我们必须更加努力地对ab之间的关系充满信心。 (事实上​​,在一般情况下,它是不可判定的!)

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

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

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

另一答案

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

GOTO

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

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;

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

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

另一答案

Donald E. Knuth在1992年的CSLI“Literate Programming”一书中回答了这个问题。在p。 17有一篇文章“while”(PDF)。我认为这篇文章也可能已在其他书籍中发表过。

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

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

另一答案

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

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

请注意,Kernighan&Plauger撰写的优秀但过时(绝版)的书籍“Structured Programming with goto Statements”包含了一些现实生活中的例子,这些例子表明GOTO在其时代(70年代后期)的编程教科书中被滥用。但是,下面的材料不是那本书。

Extended range for a DO loop

The Elements of Programming Style, 2nd Edn

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

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

Open-coded subroutine

       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

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

有比构成的goto更糟糕的构造 - 你可以为变量分配标签,然后使用指定的goto。谷歌搜索 ...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 告诉我,它已从Fortran 95中删除了。用一个结构化编程革命的粉笔,可以说是公开的Dijkstra的“GOTO Considered Harmful”字母或文章。

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

另一答案

转到认为有帮助。

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

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

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

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

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

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

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

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

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

另一答案

没有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)计算得到的结果是这样的:

bool

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

另一答案

因为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是一种原始操作,它可以实现像Gotowhile之类的东西。

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

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

平淡无奇的一面是,向上指向的goto可以实现一个完美合理的循环,向下指向的goto可以做一个完全合理的gotobreak。当然,实际的returnwhilebreak将更具可读性,因为穷人不必模拟return的效果以获得全局。所以,一般来说一个坏主意。

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

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

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