在支持循环和功能的语言中使用“ goto”是否有优势?如果是这样,为什么?


201

长期以来,我一直认为,goto如果可能,永远不要使用。前几天在阅读libavcodec(用C编写)时,我注意到它的多种用法。使用goto支持循环和功能的语言是否有优势?如果是这样,为什么?

Answers:


242

我知道有一些使用“ goto”语句的原因(有些已经对此进行了说明):

干净地退出功能

通常在函数中,您可能会分配资源,并且需要在多个位置退出。程序员可以通过将资源清除代码放在函数的末尾来简化其代码,并且该函数的所有“退出点”都将进入清除标签。这样,您不必在函数的每个“退出点”都编写清除代码。

退出嵌套循环

如果您处于嵌套循环中并且需要脱离所有循环,那么goto可以比break语句和if-checks更加简洁明了。

低级性能改进

这仅在性能至关重要的代码中有效,但是goto语句执行得非常快,并且在遍历函数时可以助您一臂之力。但是,这是一把双刃剑,因为编译器通常无法优化包含gotos的代码。

请注意,在所有这些示例中,gotos仅限于单个函数的范围。


15
退出嵌套循环的正确方法是将内部循环重构为单独的方法。
杰森

129
@Jason-Bah。那是多头。更换gotoreturn仅仅是愚蠢的。它不是在“重构”任何东西,而只是在“ goto重塑”,以便在受到压抑的环境中成长的人(即我们所有人)对使用道德上等于a的东西感觉更好goto。我更喜欢看环,我用它,看到一点点goto,这本身是只是一个工具,不是看到有人已经移到环某处无关只是一种逃避goto
克里斯·卢茨

18
在某些情况下,gotos很重要:例如,无异常的C ++环境。在Silverlight源代码中,通过使用宏,我们存在成千上万(或更多)用于安全功能的goto语句-关键的媒体编解码器和库通常通过返回值工作,而绝不会出现异常,并且很难将这些错误处理机制组合到单一表演方式。
杰夫·威尔考克斯

79
值得一提的是所有的breakcontinuereturn基本上goto,只是在漂亮的包装。
el.pescado

16
do{....}while(0)除了它在Java中起作用之外,我看不出有什么方法比goto更好。
杰里米(Jeremy List)

906

每个人都goto直接或间接地反对Edsger Dijkstra的GoTo被认为有害的文章,以证实自己的立场。太糟糕了,Dijkstra的文章实际上与如今使用语句的方式无关goto,因此,文章所说的内容几乎不适用于现代编程领域。在goto现在一个宗教稀少米姆的青草,一直到它的经文从高,其高牧师和感知异端的回避(或更糟)决定。

让我们将Dijkstra的论文放在上下文中,以阐明该主题。

当Dijkstra撰写论文时,当时的流行语言是非结构化的过程语言,例如BASIC,FORTRAN(较早的方言)和各种汇编语言。使用高级语言的人们在扭曲的,扭曲的执行线程中跳过整个代码库是很普遍的,从而产生了“意大利面条式代码”一词。您可以跳到迈克·梅菲尔德(Mike Mayfield)编写的经典《迷航(Trek)》游戏中,然后尝试弄清楚事物的工作原理,从而看到这一点。花一点时间看一下。

是Dijkstra在1968年的论文中所指责的“毫无保留地使用go to statement”。 是他所居住的环境促使他撰写该论文。他在批评中要求停止在代码中任意位置随意跳转的能力。将其与gotoC或其他此类更现代的语言的无穷能力进行比较很容易。

当他们面对异端时,我已经可以听到他们的高呼。他们喊道:“但是,您可能会使代码很难用gotoC 读取。” 哦耶?如果不这样做,可能会使代码很难阅读goto。像这个:

#define _ -F<00||--F-OO--;
int F=00,OO=00;main(){F_OO();printf("%1.3f\n",4.*-F/OO/OO);}F_OO()
{
            _-_-_-_
       _-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
        _-_-_-_-_-_-_-_
            _-_-_-_
}

不在goto视线范围内,因此必须易于阅读,对吗?还是这个呢?

a[900];     b;c;d=1     ;e=1;f;     g;h;O;      main(k,
l)char*     *l;{g=      atoi(*      ++l);       for(k=
0;k*k<      g;b=k       ++>>1)      ;for(h=     0;h*h<=
g;++h);     --h;c=(     (h+=g>h     *(h+1))     -1)>>1;
while(d     <=g){       ++O;for     (f=0;f<     O&&d<=g
;++f)a[     b<<5|c]     =d++,b+=    e;for(      f=0;f<O
&&d<=g;     ++f)a[b     <<5|c]=     d++,c+=     e;e= -e
;}for(c     =0;c<h;     ++c){       for(b=0     ;b<k;++
b){if(b     <k/2)a[     b<<5|c]     ^=a[(k      -(b+1))
<<5|c]^=    a[b<<5      |c]^=a[     (k-(b+1     ))<<5|c]
;printf(    a[b<<5|c    ]?"%-4d"    :"    "     ,a[b<<5
|c]);}      putchar(    '\n');}}    /*Mike      Laman*/

goto存在要么。因此,它必须可读。

这些例子对我有什么意义?并非语言功能会导致无法阅读,无法维护的代码。不是语法可以做到这一点。造成这种情况的是不良的程序员。正如您在上面的项目中看到的那样,不良的程序员可能会使任何语言功能都不可读也不可用。就像for那里的循环。(您可以看到它们,对吗?)

公平地说,某些语言结构比其他语言结构更容易滥用。如果你是一个C程序员,但是,我会更密切地关注的用途约50%同行#define多久,我就反对讨伐去goto

因此,对于那些不愿阅读本文的人来说,有几点要注意。

  1. Dijkstra的纸上goto声明是为编程环境下写goto了一个很大 更多潜在的破坏性比在最现代的语言不属于汇编。
  2. 因此,自动丢弃的所有用法就goto和说“我曾经尝试过一次乐趣但不喜欢它,所以现在我反对它”一样合理。
  3. 现代(厌食)goto语句在代码中有合法用途,无法用其他结构充分替代。
  4. 当然,对这些声明有非法使用。
  5. 还有现代控制语句的非法使用,例如godo“憎恶”,do它打破了总是错误的循环来break代替a goto。这些通常比明智的使用更糟糕goto

42
@pocjoc:例如,用C编写尾递归优化。这是在功能语言解释器/运行时的实现中提出的,但是代表了有趣的问题类别。因此,大多数用C编写的编译器都使用goto。它不会在大多数情况下会出现一个小众使用,当然,但在情况下,它被调用它使一个很大的意义上使用。
zxq9

66
为什么每个人都在谈论Dijkstra的“转到被认为有害的”论文而没有提及Knuth的答复:“使用go语句进行结构化编程”?
Oblomov

22
@ Nietzche-jou“这是一个相当公然的稻草人。”他讽刺地使用了错误的二分法,以显示出耻辱是不合逻辑的。稻草人是一种逻辑上的谬误,在这种情况下,您故意歪曲了对手的位置,使他们看起来很容易被击败。例如:“无神论者只是无神论者,因为他们讨厌上帝”。错误的二分法是当您假设存在至少一种其他可能性(即黑白)时,相反的极端成立。例如:“上帝讨厌同性恋,因此他爱异性恋”。
Braden Best

27
@JLRishe您正在阅读的内容太深,甚至不存在。如果使用它的人诚实地认为这是合乎逻辑的,那只是一个真正的逻辑谬论。但是他讽刺地说,清楚地表明他知道这很荒谬,这就是他展示它的方式。他不是在“用它来证明自己的观点是正确的”。他用它来显示为什么说gotos自动将代码变成意大利面条是荒谬的。也就是说,您仍然可以在没有gotos的情况下编写糟糕的代码。那是他的意思。讽刺的错误二分法是他以幽默的方式说明它的方法。
Braden Best

32
-1提出了一个非常大的论点,为什么Dijkstra的话不适用,却忘记显示出实际的优势goto(这是发布的问题)
Maarten Bodewes

154

盲目遵守最佳实践不是最佳实践。避免将goto语句作为流控制的主要形式的想法是避免产生无法阅读的意大利面条式代码。如果在正确的地方少用,它们有时可能是表达想法的最简单,最清晰的方法。Zortech C ++编译器和D编程语言的创建者Walter Bright经常但明智地使用它们。即使有这些goto语句,他的代码仍然是完全可读的。

底线:为避免goto而避免goto是没有意义的。您真正要避免的是产生无法读取的代码。如果您的goto满载代码是可读的,则没有任何问题。


36

由于goto使对程序流的推理变得困难1(又称“意大利面条代码”),goto通常仅用于补偿缺少的功能:使用goto可能实际上是可以接受的,但前提是该语言没有提供更结构化的变体来获取相同的目标。以Doubt为例:

我们使用goto的规则是goto可以跳转到函数中的单个出口清理点。

这是正确的-但前提是该语言不允许使用清理代码(例如RAII或finally)进行结构化异常处理,这样做的效果更好(因为它是专门为此目的而构建的),或者在有充分理由不这样做的情况下使用结构化异常处理(但除非是非常低的级别,否则您永远不会遇到这种情况)。

在大多数其他语言中,唯一可接受的用法goto是退出嵌套循环。甚至在那里,将外部循环提升为自己的方法并return改为使用几乎总是更好的选择。

除此之外,这goto表明没有对特定代码进行足够的思考。


1支持的现代语言goto实现了一些限制(例如,goto可能无法跳入或跳出功能),但问题从根本上保持不变。

顺便说一下,其他语言功能当然也是如此,最明显的例外是。而且通常有严格的规则来仅在指示的地方使用这些功能,例如不使用异常来控制非异常程序流的规则。


4
这里只是很好奇,但是使用gotos清理代码的情况呢?我所说的清理不仅意味着内存的重新分配,而且还包括错误记录。我正在阅读一堆帖子,很显然,没有人写出打印日志的代码。
shiva

37
“基本上,由于goto的缺陷性质(我相信这是没有争议的)” -1,我停止在那里阅读。
o0'。

14
对goto的警告来自结构化编程时代,在该时代,早日返回也被认为是邪恶的。将嵌套循环移动到一个函数中将一个“邪恶”换成另一个,并创建了一个没有真实理由存在的函数(“允许我避免使用goto!”之外)。的确,如果没有限制,goto就是产生意大利面条代码的最强大的工具,但这是一个完美的应用程序。它简化了代码。克努斯(Knuth)为这种用法辩护,迪克斯特拉(Dijkstra)说:“不要陷入相信我对goto极为教条的陷阱”。
2012年

5
finally?因此,将异常用于错误处理之外的其他东西是好的,但使用goto不好的东西呢?我认为异常的命名很恰当。
基督教徒

3
@Mud:在我每天遇到的情况下,创建“没有真正理由存在的” 函数是最好的解决方案。原因:它从顶级函数中删除了细节,因此结果具有易于阅读的控制流。我个人不会遇到goto产生可读性最高的代码的情况。另一方面,我在简单的函数中使用了多个返回值,其中其他人可能更喜欢转到单个出口点。我很想看到反例,其中goto比重构成命名函数更易读。
制造商史蒂夫(Steve)2014年

35

好吧,有一件事总是比goto's;其他程序流运算符的奇怪用法,以避免转到:

例子:

    // 1
    try{
      ...
      throw NoErrorException;
      ...
    } catch (const NoErrorException& noe){
      // This is the worst
    } 


    // 2
    do {
      ...break; 
      ...break;
    } while (false);


    // 3
    for(int i = 0;...) { 
      bool restartOuter = false;
      for (int j = 0;...) {
        if (...)
          restartOuter = true;
      if (restartOuter) {
        i = -1;
      }
    }

etc
etc

2
do{}while(false)我认为可以认为是惯用的。您不允许不同意:D
Thomas Eding

37
@trinithis:如果它是“惯用的”,那仅仅是因为反goto崇拜。如果仔细观察,您会发现它只是一种说法,goto after_do_block;而没有实际说出来。否则...只运行一次的“循环”?我称这种滥用控制结构。
cHao 2011年

5
@ThomasEding Eding您的观点有一个例外。如果您曾经做过一些C / C ++编程并且必须使用#define,那么您会知道do {} while(0)是封装多行代码的标准之一。例如:#define do {memcpy(a,b,1); something ++;} while(0)#define memcpy(a,b,1);
Ignas2526

10
@ Ignas2526您已经很好地展示了#defines比goto
不时

3
+1可以很好地列出人们尽量避免陷入困境的各种方式;我曾在其他地方提到过这种技术,但这是一个很棒的清单。考虑为什么在过去的15年中,我的团队从来不需要这些技术,也不需要使用goto。即使在具有数十万行代码的客户端/服务器应用程序中,也需要多个程序员。C#,Java,PHP,Python,JavaScript。客户端,服务器和应用程序代码。不吹牛,也没有诚恳地担任一个职位,只是真正地好奇为什么有些人遇到以乞讨为最清晰解决方案的情况,而另一些人却没有...
ToolmakerSteve

28

C#中, switch语句不允许掉线。因此,goto用于将控制权转移到特定的开关箱标签或默认标签。

例如:

switch(value)
{
  case 0:
    Console.Writeln("In case 0");
    goto case 1;
  case 1:
    Console.Writeln("In case 1");
    goto case 2;
  case 2:
    Console.Writeln("In case 2");
    goto default;
  default:
    Console.Writeln("In default");
    break;
}

编辑:“不掉线”规则有一个例外。如果case语句没有代码,则允许穿透。


3
开关落空被支撑在.NET 2.0 - msdn.microsoft.com/en-us/library/06tc147t(VS.80).aspx
rjzii

9
仅当案例没有代码主体时。如果确实有代码,则必须使用goto关键字。
马修·怀特(

26
这个答案很有趣-C#消除了失败的原因,因为许多人认为它有害,并且本示例使用goto(也被很多人视为有害)来恢复原始的,据说是有害的行为,但总体结果实际上是危害较小(因为代码清楚地表明失败是故意的!)。
thomasrutter 2010年

9
仅因为关键字写有字母GOTO并不能使其成为goto。提出的案例并非一帆风顺。它是用于失败的switch语句构造。再说一次,我对C#不太了解,所以我可能是错的。
Thomas Eding

1
好吧,在这种情况下,它远不止于失败(因为您可以goto case 5:在遇到情况1时说出来)。似乎Konrad Rudolph的答案在这里是正确的:goto正在补偿缺少的功能(并且不如实际功能清晰)。如果我们真正想要的是穿透,则最好的默认设置将是没有穿透,但是continue要明确要求它。
大卫·斯通

14

#ifdef TONGUE_IN_CHEEK

Perl有一个goto允许您实现穷人的尾声的工具。:-P

sub factorial {
    my ($n, $acc) = (@_, 1);
    return $acc if $n < 1;
    @_ = ($n - 1, $acc * $n);
    goto &factorial;
}

#endif

好吧,所以这与C无关goto。更严重的是,我同意有关goto用于清理或实现Duff设备等的其他评论。这都是关于使用,而不是滥用。

(相同的注释可用于longjmp,异常等)call/cc,它们具有合法用途,但很容易被滥用。例如,在完全非异常的情况下,纯粹为了逃避嵌套的控制结构而抛出异常)


我认为这是在Perl中使用goto的唯一原因。
布拉德·吉尔伯特

12

这些年来,我已经编写了多行汇编语言。最终,每种高级语言都可以编译成gotos。好吧,称它们为“分支”或“跳跃”或其他名称,但它们是愚蠢的。任何人都可以编写goto-less汇编程序吗?

现在确定,您可以指出一个Fortran,C或BASIC程序员,使用gotos进行暴动是意大利面条意粉的秘诀。但是,答案不是避免它们,而是要谨慎使用它们。

一把刀可以用来准备食物,释放某人或杀死某人。我们是否会因为担心刀而没有刀?同样,goto:粗心使用会阻碍,谨慎使用会有所帮助。


1
也许您想阅读为什么我认为在stackoverflow.com/questions/46586/…上
康拉德·鲁道夫

15
任何提出“无论如何都编译成JMP!”的人!该论点基本上不理解使用高级语言进行编程的意义。
尼采茹

7
您真正需要的只是减法和分支。其他一切都是为了方便或提高性能。
大卫·斯通

8

看看在C中进行编程何时使用Goto

尽管使用goto几乎总是不好的编程习惯(可以肯定的是,您可以找到一种更好的XYZ方法),但有时候确实不是一个坏选择。甚至有人认为,当它有用时,它是最佳选择。

关于goto,我要说的大多数内容实际上仅适用于C。如果您使用的是C ++,则没有合理的理由使用goto代替异常。但是,在C语言中,您没有异常处理机制的功能,因此,如果您想将错误处理与程序逻辑的其余部分分开,并且希望避免在整个代码中多次重写清理代码,那么goto可能是一个不错的选择。

我什么意思 您可能有一些类似以下的代码:

int big_function()
{
    /* do some work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* clean up*/
    return [success];
}

在您意识到需要更改清除代码之前,这很好。然后,您必须进行4个更改。现在,您可能决定只将所有清理封装到一个函数中;这不是一个坏主意。但这确实意味着您需要小心使用指针-如果您打算在清理函数中释放指针,除非您将指针传递给指针,否则无法将其设置为指向NULL。在许多情况下,无论如何您都不会再次使用该指针,因此这可能不是主要问题。另一方面,如果您添加了新的指针,文件句柄或其他需要清除的内容,则需要再次更改清除功能;然后您需要将参数更改为该函数。

通过使用goto,它将是

int big_function()
{
    int ret_val = [success];
    /* do some work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
end:
    /* clean up*/
    return ret_val;
}

这样做的好处是,您的代码尾部可以访问执行清理所需的所有内容,并且您已设法大大减少了更改点的数量。另一个好处是,您已经从功能的多个出口点变成了一个出口点。您将有可能在不清理的情况下意外退出该功能。

而且,由于goto仅用于跳转到单个点,因此并不是仿佛要创建大量意粉来回跳动来模拟函数调用。相反,goto实际上有助于编写更结构化的代码。


简而言之,goto应始终谨慎使用,并且作为最后的手段-但是有时间和地方。问题不应该是“您是否必须使用它”,而应该是“使用它是否是最佳选择”。


7

goto不好的原因之一,除了编码风格之外,您还可以使用它来创建重叠非嵌套的循环:

loop1:
  a
loop2:
  b
  if(cond1) goto loop1
  c
  if(cond2) goto loop2

这将创建一种奇怪但可能合法的控制流程结构,在该结构中可能会出现类似(a,b,c,b,a,b,a,b,...)的序列,这使编译器黑客感到不满。显然,有许多聪明的优化技巧依赖于这种类型的结构而不会发生。(我应该检查我的龙书的副本……)(使用某些编译器)的结果可能是未对包含gotos的代码进行其他优化。

如果你这可能是有用的知道,它只是“哦,对了”,恰巧编译器更快说服发出代码。就个人而言,在使用goto之类的技巧之前,我更愿意尝试向编译器解释什么是可能的,什么不是,但可以说,我也可能goto在破解汇编程序之前尝试尝试。


3
好吧……让我回到了在一家金融信息公司中对FORTRAN进行编程的时代。2007
。– Marcin

5
任何语言结构都可能被滥用,以致使其无法阅读或表现不佳。
只是我的正确观点,2010年

@JUST:重点不是可读性或性能差,而是关于控制流图的假设和保证。任何滥用goto的目的都是为了提高性能(或可读性)。
安德斯·欧仁纽斯

3
我认为有用的原因之一goto是,它允许您构造这样的循环,否则将需要很多逻辑上的扭曲。我会进一步争论说,如果优化器不知道如何重写它,那就太好了。这样的循环不应该出于提高性能或可读性的目的而进行,而是因为这正是事情发生的顺序。在这种情况下,我不希望优化器随它拧紧。
cHao 2014年

1
...将所需算法直接转换为基于GOTO的代码可能比一堆标记变量和滥用的循环结构更容易验证。
2014年

7

我觉得有些人会列举goto可以接受的情况,说所有其他用途都不可接受,我觉得很有趣。您是否真的认为您知道goto是表示算法的最佳选择的每种情况?

为了说明,我将给您一个示例,这里没有人显示:

今天,我正在编写用于在哈希表中插入元素的代码。哈希表是先前计算的缓存,可以随意覆盖(影响性能,但不影响正确性)。

哈希表的每个存储桶都有4个插槽,我有很多标准来确定存储桶已满时要覆盖哪个元素。现在,这意味着最多要通过一个存储桶进行三遍,如下所示:

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    goto add;

// Otherwise, find first empty element
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
    goto add;

// Additional passes go here...

add:
// element is written to the hash table here

现在,如果我不使用goto,那么这段代码会是什么样?

像这样:

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    break;

if (add_index >= ELEMENTS_PER_BUCKET) {
  // Otherwise, find first empty element
  for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
    if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
      break;
  if (add_index >= ELEMENTS_PER_BUCKET)
   // Additional passes go here (nested further)...
}

// element is written to the hash table here

如果添加更多的传递,情况会越来越糟,而带有goto的版本始终保持相同的缩进级别,并避免使用伪造的if语句,其结果被前一个循环的执行所隐含。

因此,在另一种情况下,goto使代码更简洁,更易于编写和理解。。。我敢肯定还有很多情况,所以不要假装知道goto有用的所有情况,而抛弃任何您可能无法使用的好方法没想到。


1
在您提供的示例中,我希望对其进行相当大的重构。通常,我会尽量避免使用单线注释来说明下一段代码在做什么。相反,我将其分解为自己的函数,该函数的名称类似于注释。如果要进行这样的转换,则此功能将概述该功能的作用,并且每个新功能都将说明您如何执行每个步骤。我认为,goto使每个函数处于相同的抽象级别比对任何反对派都重要。避免它goto是一种奖励。
大卫·斯通

2
您还没有真正解释过如何添加更多功能来摆脱goto,多个缩进级别和虚假if语句...
Ricardo 2012年

使用标准的容器符号看起来像这样:container::iterator it = slot_p.find(hash_key); if (it != slot_p.end()) it->overwrite(hash_key); else it = slot_p.find_first_empty();我发现这类程序更容易阅读。在这种情况下,每个函数都可以编写为纯函数,这样就更容易推论了。现在,主要功能仅通过功能名称说明代码的功能,然后,如果需要,您可以查看其定义以了解其功能。
大卫·斯通

3
任何人都必须举一些例子,说明某些算法自然应该使用goto的事实-这令人遗憾地反思了今天的算法思考很少!当然,@ Ricardo的示例是goto优雅且显而易见的完美示例(众多示例之一)。
Fattie 2014年

6

我们使用goto的规则是goto可以跳转到函数中的单个出口清理点。在非常复杂的功能中,我们放宽该规则以允许其他跳转。在这两种情况下,我们都避免在错误代码检查中经常出现的嵌套if语句,这有助于提高可读性和可维护性。


1
我可以看到类似的东西在像C这样的语言中很有用。但是,当您拥有C ++构造函数/析构函数的功能时,通常没有那么有用。
大卫·斯通

1
“在真正复杂的函数中,我们放宽了该规则以允许其他跳转。”如果不使用示例,那么通过使用跳转使复杂函数变得更加复杂,这听起来像是。重构和拆分那些复杂的功能不是“更好”的方法吗?
MikeMB 2015年

1
这是我使用goto的最后一个目的。我不会错过Java中的goto,因为它可以最终尝试完成相同的工作。
Patricia Shanahan

5

goto语句,其合法的用途,和替代结构,可以代替“良性goto语句”中使用,但可能会被滥用一样容易goto语句,是高德纳的文章“最周到和全面的讨论与goto语句结构化编程 ” ,1974年12月的《计算机调查》(第6卷,第4期,第261-301页)。

毫不奇怪,这份已有39年历史的论文的某些方面是过时的:处理能力的数量级增加使Knuth的性能改进在中等大小的问题上不明显,并且从那时起发明了新的编程语言结构。(例如,try-catch块包含Zahn的Construct,尽管很少以这种方式使用。)但是Knuth涵盖了论点的方方面面,在任何人再次讨论该问题之前,都应阅读此书。


3

在Perl模块中,您偶尔需要动态创建子例程或闭包。问题是,一旦创建了子例程,您将如何获得它。您可以调用它,但是如果子例程使用caller()它,将不会像它那样有用。这就是goto &subroutine变化可能会有所帮助的地方。

这是一个简单的示例:

sub AUTOLOAD{
  my($self) = @_;
  my $name = $AUTOLOAD;
  $name =~ s/.*:://;

  *{$name} = my($sub) = sub{
    # the body of the closure
  }

  goto $sub;

  # nothing after the goto will ever be executed.
}

您也可以使用的这种形式goto提供尾调用优化的基本形式。

sub factorial($){
  my($n,$tally) = (@_,1);

  return $tally if $n <= 1;

  $tally *= $n--;
  @_ = ($n,$tally);
  goto &factorial;
}

(在Perl 5版本16中,最好写成goto __SUB__;

有一个模块会导入tail修饰符,recur如果您不喜欢使用这种形式的,则会导入一个修饰符goto

use Sub::Call::Tail;
sub AUTOLOAD {
  ...
  tail &$sub( @_ );
}

use Sub::Call::Recur;
sub factorial($){
  my($n,$tally) = (@_,1);

  return $tally if $n <= 1;
  recur( $n-1, $tally * $n );
}

使用其他大多数原因goto最好与其他关键字一起使用。

喜欢redo一些代码:

LABEL: ;
...
goto LABEL if $x;
{
  ...
  redo if $x;
}

last从多个位置转到一些代码:

goto LABEL if $x;
...
goto LABEL if $y;
...
LABEL: ;
{
  last if $x;
  ...
  last if $y
  ...
}

2

如果是这样,为什么?

C没有多级/标记中断,并且并非所有控制流都可以使用C的迭代和决策原语轻松建模。Goto在解决这些缺陷方面大有帮助。

有时使用某种标志变量来实现某种伪多级中断更为清晰,但是它并不总是优于goto(至少goto允许人们轻松地确定控制权去向何处,这与flag变量不同),有时您根本不想支付标志/其他扭曲的性能价格来避免使用goto。

libavcodec是对性能敏感的代码。直接表达控制流可能是一个优先事项,因为它会更好地运行。


2

同样没有人实现过“ COME FROM”语句。


8
或在C ++,C#,Java,JS,Python,Ruby .... etc等中,只有他们称其为“例外”。
cHao 2011年

2

我发现do {} while(false)用法完全令人反感。可以想到的是,在某些奇怪的情况下,我有必要说服我这样做,但是从来没有说过这是干净易懂的代码。

如果必须执行这样的循环,为什么不明确声明对flag变量的依赖性?

for (stepfailed=0 ; ! stepfailed ; /*empty*/)

/*empty*/不是stepfailed = 1吗?无论如何,这比a更好do{}while(0)吗?在这两种情况下,您都需要break退出(或退出stepfailed = 1; continue;)。对我来说似乎不必要。
Thomas Eding

2

1)我所知道的goto最常见的用法是使用不提供它的语言(即C)来模拟异常处理。我会看到这样使用了无数个笨蛋;根据2013年进行的一项快速调查:http://blog.regehr.org/archives/894,Linux代码中大约有100,000个goto 。Linux编码风格指南中甚至提到了Goto用法:https : //www.kernel.org/doc/Documentation/CodingStyle。就像使用填充有函数指针的结构来模拟面向对象的编程一样,goto在C编程中也占有一席之地。那么谁是对的:Dijkstra或Linus(以及所有Linux内核编码器)?基本上是理论与实践。

但是,通常的陷阱是没有编译器级别的支持,也没有检查常见的构造/模式:更容易错误地使用它们并引入错误,而无需进行编译时检查。Windows和Visual C ++但在C模式下通过SEH / VEH提供异常处理,这正是出于这个原因:即使在OOP语言之外(即在过程语言中),异常也很有用。但是,即使编译器为语言中的异常提供了语法支持,也无法始终保存您的培根。以后一种情况为例,著名的Apple SSL“ goto失败”错误仅复制了一个goto,造成了灾难性的后果(https://www.imperialviolet.org/2014/02/22/applebug.html):

if (something())
  goto fail;
  goto fail; // copypasta bug
printf("Never reached\n");
fail:
  // control jumps here

使用编译器支持的异常,例如在C ++中,您可能会遇到完全相同的错误:

struct Fail {};

try {
  if (something())
    throw Fail();
    throw Fail(); // copypasta bug
  printf("Never reached\n");
}
catch (Fail&) {
  // control jumps here
}

但是,如果编译器分析并警告您有关无法访问的代码,则可以避免两种错误​​。例如,在/ W4警告级别使用Visual C ++进行编译在两种情况下都会发现该错误。例如,Java出于一个很好的理由禁止无法访问的代码(可以在其中找到它!):它可能是普通Joe的代码中的错误。只要goto构造不允许编译器无法轻易找出的目标(例如将goto转换为计算地址(**)),对于使用gotos的函数在编译器中查找无法到达的代码,比使用Dijkstra难得多。批准的代码。

(**)脚注:在某些Basic版本中,可以转到计算的行号,例如GOTO 10 * x,其中x是变量。令人困惑的是,在Fortran中,“ compute goto”是指等效于C中的switch语句的构造。标准C不允许使用该语言中的已计算的goto,而仅允许goto静态/语法声明的标签。但是,GNU C进行了扩展以获取标签的地址(一元,前缀&&运算符),并且还允许转到类型为void *的变量。有关这个晦涩的子主题的更多信息,请参见https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html。这篇文章的其余部分与那个晦涩的GNU C功能无关。

标准C(即未计算的)getos通常不是在编译时找不到无法到达的代码的原因。通常的原因是如下所示的逻辑代码。给定

int computation1() {
  return 1;
}

int computation2() {
  return computation1();
}

对于编译器而言,要在以下3种构造中找到无法到达的代码同样困难:

void tough1() {
  if (computation1() != computation2())
    printf("Unreachable\n");
}

void tough2() {
  if (computation1() == computation2())
    goto out;
  printf("Unreachable\n");
out:;
}

struct Out{};

void tough3() {
  try {
    if (computation1() == computation2())
      throw Out();
    printf("Unreachable\n");
  }
  catch (Out&) {
  }
}

(请原谅我与花括号相关的编码样式,但我尝试使这些示例尽可能紧凑。)

Visual C ++ / W4(甚至使用/ Ox)也无法在其中任何一个中找到无法访问的代码,并且您可能已经知道,通常无法确定查找无法访问的代码的问题。(如果您不相信我的话:https : //www.cl.cam.ac.uk/teaching/2006/OptComp/slides/lecture02.pdf

作为一个相关问题,C goto只能用于在函数体内模拟异常。标准C库提供了setjmp()和longjmp()对函数来模拟非本地出口/异常,但是与其他语言相比,它们具有一些严重的缺点。Wikipedia的文章http://en.wikipedia.org/wiki/Setjmp.h很好地解释了后面的问题。该功能对也可以在Windows(http://msdn.microsoft.com/zh-cn/library/yz2ez4as.aspx)上运行,但是几乎没有人在此使用它们,因为SEH / VEH更为出色。即使在Unix上,我也很少使用setjmp和longjmp。

2)我认为C语言中goto的第二个最常见用法是实现多级中断或多级继续,这也是一个毫无争议的用例。回想一下Java不允许goto标签,但允许break标签或Continue标签。根据http://www.oracle.com/technetwork/java/simple-142616.html,这实际上是C语言中gotos的最常见用例(他们说90%),但是根据我的主观经验,系统代码倾向于经常使用gotos进行错误处理。也许在科学代码中,或者在OS提供异常处理(Windows)的地方,多级退出是主要的用例。他们实际上没有提供有关调查背景的任何细节。

编辑添加:事实证明,这两种使用模式在第60页左右的Kernighan和Ritchie的C书中找到(取决于版本)。值得注意的另一件事是,两个用例都只涉及正向指令。事实证明,MISRA C 2012版(与2004版不同)现在允许gotos,只要它们只是向前的即可。


对。“房间里的大象”是Linux内核,出于举足轻重的缘故,它是举世闻名的代码库的一个示例,其中装有goto。当然是的。明显。“反gome”只是几十年前的一种好奇。当然,编程中有很多事情(非静态问题,尤其是“全局变量”,例如“ elseif”)可能会被非专业人士滥用。因此,如果您是表弟的Learnin 2程序,则告诉他们“哦,不要使用全局变量”和“不要使用elseif”。
Fattie 2014年

goto失败错误与goto无关。该问题是由if语句之后没有大括号引起的。几乎任何粘贴两次的语句副本都会造成问题。我认为这种裸露的裸露方式比goto有害得多​​。
muusbolla

2

有人说在C ++中没有理由使用goto。有人说在99%的情况下有更好的选择。这不是推理,只是不合理的印象。这是一个可靠的示例,其中goto导致了一个不错的代码,类似于增强的do-while循环:

int i;

PROMPT_INSERT_NUMBER:
  std::cout << "insert number: ";
  std::cin >> i;
  if(std::cin.fail()) {
    std::cin.clear();
    std::cin.ignore(1000,'\n');
    goto PROMPT_INSERT_NUMBER;          
  }

std::cout << "your number is " << i;

将其与免费代码进行比较:

int i;

bool loop;
do {
  loop = false;
  std::cout << "insert number: ";
  std::cin >> i;
  if(std::cin.fail()) {
    std::cin.clear();
    std::cin.ignore(1000,'\n');
    loop = true;          
  }
} while(loop);

std::cout << "your number is " << i;

我看到了这些差异:

  • {}需要嵌套块(尽管do {...} while看起来更熟悉)
  • 需要额外的loop变量,在四个地方使用
  • 花费较长的时间阅读和理解 loop
  • loop不包含任何数据,它只是控制执行的流程,这比简单的标签不太理解

还有另一个例子

void sort(int* array, int length) {
SORT:
  for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
    swap(data[i], data[i+1]);
    goto SORT; // it is very easy to understand this code, right?
  }
}

现在让我们摆脱“邪恶的” goto:

void sort(int* array, int length) {
  bool seemslegit;
  do {
    seemslegit = true;
    for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
      swap(data[i], data[i+1]);
      seemslegit = false;
    }
  } while(!seemslegit);
}

您会看到它与goto的使用类型相同,它的结构合理,并且不像推荐的唯一方法那样前进goto。您肯定要避免这样的“智能”代码:

void sort(int* array, int length) {
  for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
    swap(data[i], data[i+1]);
    i = -1; // it works, but WTF on the first glance
  }
}

关键是goto容易被滥用,但是goto本身不应该受到指责。请注意,label在C ++中具有函数作用域,因此它不会像在纯汇编中那样污染全局作用域,在纯汇编中重叠循环有它的位置并且非常常见-就像在8051的以下代码中,其中7段显示器连接到P1。该程序在以下位置循环闪电段:

; P1 states loops
; 11111110 <-
; 11111101  |
; 11111011  |
; 11110111  |
; 11101111  |
; 11011111  |
; |_________|

init_roll_state:
    MOV P1,#11111110b
    ACALL delay
next_roll_state:
    MOV A,P1
    RL A
    MOV P1,A
    ACALL delay
    JNB P1.5, init_roll_state
    SJMP next_roll_state

还有一个优点:goto可以用作命名循环,条件和其他流:

if(valid) {
  do { // while(loop)

// more than one page of code here
// so it is better to comment the meaning
// of the corresponding curly bracket

  } while(loop);
} // if(valid)

或者,您可以使用带有缩进的等效goto,因此,如果明智地选择标签名称,则无需注释:

if(!valid) goto NOTVALID;
  LOOPBACK:

// more than one page of code here

  if(loop) goto LOOPBACK;
NOTVALID:;

1

在Perl中,使用标签从循环中“转到”-使用“ last”语句,这与break相似。

这样可以更好地控制嵌套循环。

也支持传统的goto 标签,但是我不确定有太多实例是这是实现所需功能的唯一方法-子例程和循环在大多数情况下就足够了。


我认为您将在Perl中使用的goto唯一形式是goto &subroutine。它以当前的@_开头子例程,同时替换堆栈中的当前子例程。
布拉德·吉尔伯特

1

“ goto”和“ goto-less编程”运动的最重要论据的问题是,如果使用得太频繁,您的代码虽然可能会正确运行,但变得难以阅读,无法维护,无法查看等。在99.99%的情况“转到”会导致意大利面条式代码。就我个人而言,我无法想到有什么很好的理由来说明为什么要使用“ goto”。


11
尽管我更喜欢术语“缺乏想象力的证明”,但在您的论点中说“我无法想到任何理由”是en.wikipedia.org/wiki/Argument_from_ignorance
我的正确观点

2
@只是我的正确观点:只有事后雇用,这才是逻辑上的谬误。从语言设计者的角度来看,权衡添加功能(goto)的成本可能是一个有效的论点。@cschol的用法类似:虽然可能现在不设计语言,但他基本上是在评估设计师的工作。
康拉德·鲁道夫

1
@KonradRudolph:恕我直言,在允许goto变量存在的环境中使用语言允许比尝试支持某人可能需要的每种控制结构便宜。用编写代码goto可能不如使用其他结构好,但是使用编写此类代码goto将有助于避免“表现力方面的漏洞”,即一种语言无法编写有效代码的构造。
2014年

1
@supercat恐怕我们来自根本不同的语言设计学院。我反对以牺牲易懂性(或正确性)为代价最大程度地表达语言。我宁愿使用限制性语言而不是宽松语言。
康拉德·鲁道夫2014年

1
@thb当然可以。这往往使代码太多难以阅读和推理。实际上,每当有人goto在代码检查站点上发布包含代码的代码时,就省去了goto大大简化代码的逻辑。
康拉德·鲁道夫

1

当然可以使用GOTO,但是有比代码样式更重要的一件事,或者在使用时必须牢记代码是否可读:内部代码可能不如您健壮想

例如,查看以下两个代码段:

If A <> 0 Then A = 0 EndIf
Write("Value of A:" + A)

与GOTO等效的代码

If A == 0 Then GOTO FINAL EndIf
   A = 0
FINAL:
Write("Value of A:" + A)

我们认为的第一件事是,代码的两个位的结果将是“ A的值:0”(当然,我们假设执行时没有并行性)

这是不正确的:在第一个示例中,A将始终为0,但在第二个示例中(使用GOTO语句),A可能不会为0。为什么?

原因是因为从程序的另一个角度来看,我可以插入a GOTO FINAL而无需控制A的值。

这个例子非常明显,但是随着程序变得越来越复杂,看到这类东西的难度也会增加。

有关材料可以在迪克斯特拉先生的著名文章“反对GO声明的案例”中找到。


6
在老式的BASIC中经常是这种情况。但是,在现代变体中,不允许跳入另一个函数的中间……或者在许多情况下,甚至不允许声明变量。基本上(双关语不是故意的),现代语言在很大程度上消除了Dijkstra所说的“毫不费力”的GOTO ...而他所反对的唯一使用它的方法就是犯下某些其他令人发指的罪行。:)
cHao 2014年

1

在以下情况下,我将使用goto:在需要从不同位置的函数返回时,以及在返回之前,需要完成一些未初始化的操作:

非goto版本:

int doSomething (struct my_complicated_stuff *ctx)    
{
    db_conn *conn;
    RSA *key;
    char *temp_data;
    conn = db_connect();  


    if (ctx->smth->needs_alloc) {
      temp_data=malloc(ctx->some_size);
      if (!temp_data) {
        db_disconnect(conn);
        return -1;      
        }
    }

    ...

    if (!ctx->smth->needs_to_be_processed) {
        free(temp_data);    
        db_disconnect(conn);    
        return -2;
    }

    pthread_mutex_lock(ctx->mutex);

    if (ctx->some_other_thing->error) {
        pthread_mutex_unlock(ctx->mutex);
        free(temp_data);
        db_disconnect(conn);        
        return -3;  
    }

    ...

    key=rsa_load_key(....);

    ...

    if (ctx->something_else->error) {
         rsa_free(key); 
         pthread_mutex_unlock(ctx->mutex);
         free(temp_data);
         db_disconnect(conn);       
         return -4;  
    }

    if (ctx->something_else->additional_check) {
         rsa_free(key); 
         pthread_mutex_unlock(ctx->mutex);
         free(temp_data);
         db_disconnect(conn);       
         return -5;  
    }


    pthread_mutex_unlock(ctx->mutex);
    free(temp_data);    
    db_disconnect(conn);    
    return 0;     
}

转到版本:

int doSomething_goto (struct my_complicated_stuff *ctx)
{
    int ret=0;
    db_conn *conn;
    RSA *key;
    char *temp_data;
    conn = db_connect();  


    if (ctx->smth->needs_alloc) {
      temp_data=malloc(ctx->some_size);
      if (!temp_data) {
            ret=-1;
           goto exit_db;   
          }
    }

    ...

    if (!ctx->smth->needs_to_be_processed) {
        ret=-2;
        goto exit_freetmp;      
    }

    pthread_mutex_lock(ctx->mutex);

    if (ctx->some_other_thing->error) {
        ret=-3;
        goto exit;  
    }

    ...

    key=rsa_load_key(....);

    ...

    if (ctx->something_else->error) {
        ret=-4;
        goto exit_freekey; 
    }

    if (ctx->something_else->additional_check) {
        ret=-5;
        goto exit_freekey;  
    }

exit_freekey:
    rsa_free(key);
exit:    
    pthread_mutex_unlock(ctx->mutex);
exit_freetmp:
    free(temp_data);        
exit_db:
    db_disconnect(conn);    
    return ret;     
}

当您需要更改释放语句中的某些内容时,第二个版本使操作变得更容易(每个代码在代码中使用一次),并减少了在添加新分支时跳过其中任何一条的机会。将它们移动到函数中将无济于事,因为可以在不同的“级别”进行取消分配。


3
这就是为什么我们finally在C#中有块
John Saunders

^就像@JohnSaunders说的那样。这是“由于语言缺乏适当的控制结构而使用goto”的示例。但是,在返回处需要多个goto点是代码的味道。有一种编程风格,更安全(更容易不搞砸)不需要goto方法,甚至在语言中能正常工作没有“终于”:设计的“清理”呼叫,使得它们是无害的,当没有干净的做需要。除了清除之外,将所有其他因素都分解为使用多次返回设计的方法。调用该方法,然后执行清理调用。
ToolmakerSteve 2014年

请注意,我描述的方法需要额外的方法调用级别(但仅在缺少的语言中finally)。或者,使用gotos,但使用一个公共出口点,该出口点始终进行所有清理。但是,每种清除方法都可以处理为null或已经清除的值,或者受条件测试保护,因此在不适当的情况下可以跳过。
制造商

@ToolmakerSteve这不是代码的味道;实际上,它是C语言中一种非常常见的模式,被认为是使用goto的最有效方法之一。您想让我创建5个方法,每个方法都有自己不必要的if-test,只是为了处理从该函数进行的清理?您现在已经创建了代码并提高了性能。或者,您可以只使用goto。
muusbolla

@muusbolla - 1)“创建5层的方法” -号我建议一个单一方法,其清理具有非空值的任何资源。或查看我的替代方法,它使用gotos都到达相同的出口点,该出口点具有相同的逻辑(如您所说,如果每个资源都需要一个额外的逻辑)。但是没关系,当使用C正确的代码时-不管代码在C语言中的原因为何,几乎可以肯定的是,这种折衷选择了最“直接”的代码。(我的建议处理的是可能分配给定资源或可能未分配给任何资源的复杂情况。但是,是的,在这种情况下过度使用了。)
ToolmakerSteve19年

0

在该领域做出了重大贡献的计算机科学家Edsger Dijkstra也因批评GoTo的使用而闻名。关于Wikipedia的论点有一篇简短的文章。


0

它对于不时进行字符字符串处理非常有用。

试想像这样的printf式示例:

for cur_char, next_char in sliding_window(input_string) {
    if cur_char == '%' {
        if next_char == '%' {
            cur_char_index += 1
            goto handle_literal
        }
        # Some additional logic
        if chars_should_be_handled_literally() {
            goto handle_literal
        }
        # Handle the format
    }
    # some other control characters
    else {
      handle_literal:
        # Complicated logic here
        # Maybe it's writing to an array for some OpenGL calls later or something,
        # all while modifying a bunch of local variables declared outside the loop
    }
}

您可以将其重构goto handle_literal为函数调用,但是如果要修改几个不同的局部变量,则除非您的语言支持可变闭包,否则您必须传递对每个局部变量的引用。continue如果逻辑使其他情况不起作用,则在调用后仍必须使用语句(可以说是goto的一种形式)以获取相同的语义。

在词法分析器中,我也明智地使用了goto,通常用于类似情况。大多数时候您不需要它们,但是对于那些奇怪的情况,它们很好。

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.