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

时间:2020-03-05 18:42:26  来源:igfitidea点击:

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

解决方案

回答

在Perl中,使用标签从循环中使用" last"语句"转到",这类似于break。

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

传统的goto标签也受支持,但是我不确定是否有太多实例是唯一实现子例程和循环满足大多数情况的唯一方法。

回答

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

回答

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

回答

由于goto使程序流程难以进行推理(又称意大利面条代码),因此goto通常仅用于补偿缺少的功能:实际上可以接受goto的使用,但前提是该语言不提供该功能。以获得相同目标的更结构化的变体。以"怀疑"为例:

The rule with goto that we use is that goto is okay to for jumping forward to a single exit cleanup point in a function.

这是正确的,但前提是该语言不允许使用清理代码(例如RAII或者" finally")进行结构化异常处理,而清理代码可以更好地完成相同的工作(因为它是专门为此目的而构建的),或者不使用结构化异常处理的原因(但除非是非常低的级别,否则我们永远不会遇到这种情况)。

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

除此之外," goto"是没有足够的思想进入特定代码段的迹象。

1支持goto的现代语言实现了一些限制(例如,goto可能不会跳入或者跳出函数),但问题从根本上仍然是相同的。

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

回答

我知道有一些使用" goto"语句的原因(有些已经讲过):

干净地退出功能

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

退出嵌套循环

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

低级性能改进

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

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

回答

在Cswitch语句中不允许掉线。因此,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语句没有代码,则允许穿透。

回答

If so, why?

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

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

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

回答

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

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

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

如果我们只是知道"哦,顺便说一句"而恰好说服编译器发出更快的代码,这可能会很有用。就个人而言,在使用goto之类的技巧之前,我更愿意尝试向编译器解释什么是可能的,什么不是,但是可以说,在黑客入侵汇编程序之前,我也可以尝试goto

回答

#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等),它们具有合法用途,但很容易被滥用。例如,抛出异常纯粹是为了逃避深层嵌套的控件结构,在完全非例外的情况下。)

回答

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

回答

在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 __;)

如果我们不喜欢使用这种形式的goto,则有一个模块将导入" tail"修饰符,而一个模块将导入" recur"。

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的大多数其他原因最好与其他关键字一起使用。

就像重做一些代码一样:

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

或者从多个位置转到最后一点代码:

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

回答

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

回答

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

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

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

回答

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

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

回答

我觉得很有趣,有人会列举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有用的所有情况,而抛弃任何我们可能无法使用的好方法没想到。

回答

好吧,有一件事总是比" goto"糟糕。奇怪地使用其他程序流运算符来避免转到:

例子:

// 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

回答

每个反对" goto"的人都直接或者间接地引用了Edsger Dijkstra的《 GoTo被认为有害》一文,以证实自己的立场。太糟糕了,Dijkstra的文章实际上与最近使用" goto"语句的方式无关,因此该文章所说的内容几乎不适用于现代编程领域。现在,"缺乏goto"的模因在一种宗教上趋于正确,直到其经文由高位,大祭司以及被察觉的(或者什至更糟的)异教徒所决定。

让我们将Dijkstra的论文放在上下文中,以便对该主题有所了解。

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

这是Dijkstra在1968年的论文中所指责的"毫无限制地使用go to statement"。这是他所居住的环境促使他撰写该论文。在任何时候,只要我们喜欢,就可以在代码中随意跳转到任何地方,这就是他批评并要求停止的地方。将其与C或者其他此类更现代的语言中的" goto"的无穷能力进行比较是很容易的。

面对异端的信徒们,我已经能听到他们的吟唱。他们喊道:"但是,使用C语言中的goto可以使代码很难阅读。"哦耶?如果没有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程序员,那么在抵制goto的努力之前,我会更密切地凝视着#define的50%的使用!

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

  • Dijkstra在" goto"语句上的论文是为一种编程环境编写的,与大多数非汇编语言中的现代语言相比," goto"具有更大的潜在破坏性。
  • 因此,自动放弃使用goto的所有用法与说"我曾经尝试过一次乐趣但不喜欢它,所以现在我反对它"一样合理。
  • 现代(厌食)" goto"语句在代码中有合法用途,无法用其他结构充分替代。
  • 当然,这些表述是非法使用的。
  • 现代控制语句也有非法使用,例如" godo"可憎性,其中总是错误的do循环被打破,而不是使用break代替goto。这些通常比明智地使用goto更糟糕。

请记住,当我们以-1的比例否决我时,我在过去15-20年中在自己的(非汇编程序)代码中正好使用3次goto。

我等待着愤怒的尖叫声和-1票充斥着喘息的洪流。

回答

我发现do {} while(false)用法完全令人反感。可以想到的是,在某些奇怪的情况下,我有必要说服我这样做,但是永远不要说它是干净的明智代码。

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

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