我一直有一个印象,如果可能的话,永远不要使用goto。
然而,在前几天阅读libavcodec(它是用C编写的)时,我惊讶地注意到它的多种用法。
在支持循环和函数的语言中使用goto是否有优势?如果有,为什么?请提供一个具体的例子,清楚地说明使用goto的理由。
我一直有一个印象,如果可能的话,永远不要使用goto。
然而,在前几天阅读libavcodec(它是用C编写的)时,我惊讶地注意到它的多种用法。
在支持循环和函数的语言中使用goto是否有优势?如果有,为什么?请提供一个具体的例子,清楚地说明使用goto的理由。
当前回答
使用“goto”使你的代码更易于阅读或运行得更快。只是不要让它把你的代码变成意大利面条。
其他回答
它有时在按字符进行字符串处理时很方便。
想象一下这样一个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重构为一个函数调用,但如果它修改了几个不同的局部变量,则必须将引用传递给每个局部变量,除非您的语言支持可变闭包。如果您的逻辑使else case不起作用,那么您仍然必须在调用之后使用continue语句(可以说是goto的一种形式)以获得相同的语义。
我还在lexer中明智地使用了gotos,通常用于类似的情况。大多数时候你不需要它们,但在一些奇怪的情况下有它们很好。
我遇到过goto是一个很好的解决方案的情况,我在这里或其他地方都没有看到过这个例子。
我有一个开关的情况下,所有的情况都需要调用相同的函数在最后。我有其他的情况,最后都需要调用不同的函数。
这看起来有点像这样:
switch( x ) {
case 1: case1() ; doStuffFor123() ; break ;
case 2: case2() ; doStuffFor123() ; break ;
case 3: case3() ; doStuffFor123() ; break ;
case 4: case4() ; doStuffFor456() ; break ;
case 5: case5() ; doStuffFor456() ; break ;
case 6: case6() ; doStuffFor456() ; break ;
case 7: case7() ; doStuffFor789() ; break ;
case 8: case8() ; doStuffFor789() ; break ;
case 9: case9() ; doStuffFor789() ; break ;
}
我没有给每个case一个函数调用,而是用goto代替了break。goto跳转到一个标签,这也是在开关的情况下。
switch( x ) {
case 1: case1() ; goto stuff123 ;
case 2: case2() ; goto stuff123 ;
case 3: case3() ; goto stuff123 ;
case 4: case4() ; goto stuff456 ;
case 5: case5() ; goto stuff456 ;
case 6: case6() ; goto stuff456 ;
case 7: case7() ; goto stuff789 ;
case 8: case8() ; goto stuff789 ;
case 9: case9() ; goto stuff789 ;
stuff123: doStuffFor123() ; break ;
stuff456: doStuffFor456() ; break ;
stuff789: doStuffFor789() ; break ;
}
案例1到3都必须调用doStuffFor123(),类似的案例4到6必须调用doStuffFor456()等。
在我看来,如果你正确使用它们,gotos是非常好的。最后,任何代码都像人们写的那样清晰。有了gotos,你可以写出意大利面代码,但这并不意味着gotos是意大利面代码的原因。这个事业就是我们;程序员。如果我愿意,我也可以用函数创建意大利面条代码。宏也是如此。
因为goto使得程序流的推理变得困难。“意大利面条代码”),goto通常只用于弥补缺失的功能:使用goto实际上可能是可以接受的,但前提是语言没有提供更结构化的变体来获得相同的目标。以《怀疑》为例:
我们使用的goto规则是,goto可以跳转到函数中的单个退出清理点。
这是对的——但前提是语言不允许使用清理代码进行结构化异常处理(如RAII或finally),后者可以更好地完成相同的工作(因为它是专门为此而构建的),或者有很好的理由不使用结构化异常处理(但除非在非常低的级别,否则您永远不会遇到这种情况)。
在大多数其他语言中,goto唯一可接受的用法是退出嵌套循环。即使在这种情况下,将外部循环提升为自己的方法并使用return也总是更好。
除此之外,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等等——它们有合法的用途,但很容易被滥用。例如,在完全非异常的情况下,抛出异常纯粹是为了转义深度嵌套的控制结构。)
在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
...
}