我一直有一个印象,如果可能的话,永远不要使用goto。
然而,在前几天阅读libavcodec(它是用C编写的)时,我惊讶地注意到它的多种用法。
在支持循环和函数的语言中使用goto是否有优势?如果有,为什么?请提供一个具体的例子,清楚地说明使用goto的理由。
我一直有一个印象,如果可能的话,永远不要使用goto。
然而,在前几天阅读libavcodec(它是用C编写的)时,我惊讶地注意到它的多种用法。
在支持循环和函数的语言中使用goto是否有优势?如果有,为什么?请提供一个具体的例子,清楚地说明使用goto的理由。
当前回答
我们使用的goto规则是,goto可以跳转到函数中的单个退出清理点。在真正复杂的函数中,我们放松了这个规则,允许其他跳转。在这两种情况下,我们都避免了经常在错误代码检查中出现的深度嵌套的if语句,这有助于可读性和维护。
其他回答
我发现有趣的是,有些人会给出一个可以接受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有用的例子,而轻视任何你想不到的好例子。
Everybody who is anti-goto cites, directly or indirectly, Edsger Dijkstra's GoTo Considered Harmful article to substantiate their position. Too bad Dijkstra's article has virtually nothing to do with the way goto statements are used these days and thus what the article says has little to no applicability to the modern programming scene. The goto-less meme verges now on a religion, right down to its scriptures dictated from on high, its high priests and the shunning (or worse) of perceived heretics.
让我们把Dijkstra的论文放在背景中,对这个问题有一些了解。
When Dijkstra wrote his paper the popular languages of the time were unstructured procedural ones like BASIC, FORTRAN (the earlier dialects) and various assembly languages. It was quite common for people using the higher-level languages to jump all over their code base in twisted, contorted threads of execution that gave rise to the term "spaghetti code". You can see this by hopping on over to the classic Trek game written by Mike Mayfield and trying to figure out how things work. Take a few moments to look that over.
这就是Dijkstra在1968年的论文中谴责的“毫无节制地使用go to语句”。这就是他所生活的环境,促使他写出了那篇论文。在你的代码中,在你喜欢的任何地方跳转的能力是他所批评和要求停止的。将其与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*/
也不去那里。因此它必须是可读的。
我举这些例子的重点是什么?并不是语言特性导致代码不可读、不可维护。这不是语法造成的。这是糟糕的程序员造成的。而糟糕的程序员,正如你在上面的项目中所看到的,可以使任何语言特性无法阅读和使用。比如上面的for循环。(你能看到他们,对吧?)
公平地说,有些语言结构比其他结构更容易被滥用。然而,如果你是一个C程序员,我会更仔细地观察#define大约50%的使用情况,然后才会开始反对goto!
因此,对于那些已经阅读到这里的人来说,有几个关键点需要注意。
Dijkstra's paper on goto statements was written for a programming environment where goto was a lot more potentially damaging than it is in most modern languages that aren't an assembler. Automatically throwing away all uses of goto because of this is about as rational as saying "I tried to have fun once but didn't like it so now I'm against it". There are legitimate uses of the modern (anaemic) goto statements in code that cannot be adequately replaced by other constructs. There are, of course, illegitimate uses of the same statements. There are, too, illegitimate uses of the modern control statements like the "godo" abomination where an always-false do loop is broken out of using break in place of a goto. These are often worse than judicious use of goto.
下面是我所知道的使用“goto”语句的一些原因(有些人已经谈到了这个问题):
干净地退出函数
通常在一个函数中,您可能会分配资源并需要在多个位置退出。程序员可以通过将资源清理代码放在函数的末尾来简化他们的代码,并且函数的所有“出口点”都将进入清理标签。这样,您就不必在函数的每个“退出点”都编写清理代码。
退出嵌套循环
如果处于嵌套循环中,需要跳出所有循环,那么goto可以比break语句和If -checks更简洁。
低水平的性能改进
这只在对性能要求严格的代码中有效,但是goto语句执行得非常快,并且可以在遍历函数时提高性能。然而,这是一把双刃剑,因为编译器通常不能优化包含goto的代码。
注意,在所有这些示例中,gotos都被限制在单个函数的范围内。
盲目地遵循最佳实践并不是最佳实践。避免将goto语句作为流控制的主要形式是为了避免产生难以阅读的意大利面条代码。如果在正确的地方谨慎使用,它们有时可以成为表达想法的最简单、最清晰的方式。Walter Bright, Zortech c++编译器和D编程语言的创造者,经常使用它们,但很明智。即使使用了goto语句,他的代码仍然完全可读。
底线:为了避免去那里而避免去那里是毫无意义的。您真正需要避免的是生成不可读的代码。如果充满goto的代码是可读的,那么它就没有任何问题。
在Perl模块中,有时希望动态地创建子例程或闭包。问题是,一旦你创建了子例程,你如何得到它。你可以直接调用它,但是如果子例程使用caller(),那么它就没有那么有用了。这就是goto &子例程变化可能有用的地方。
这里有一个简单的例子:
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 version 16中,最好写成goto __SUB__;)
有一个模块会导入尾修饰符,如果你不喜欢使用这种形式的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的大多数其他原因都可以用其他关键字更好地完成。
比如重写一段代码:
LABEL: ;
...
goto LABEL if $x;
{
...
redo if $x;
}
或者从多个地方找到最后一段代码:
goto LABEL if $x;
...
goto LABEL if $y;
...
LABEL: ;
{
last if $x;
...
last if $y
...
}