我一直有一个印象,如果可能的话,永远不要使用goto。
然而,在前几天阅读libavcodec(它是用C编写的)时,我惊讶地注意到它的多种用法。
在支持循环和函数的语言中使用goto是否有优势?如果有,为什么?请提供一个具体的例子,清楚地说明使用goto的理由。
我一直有一个印象,如果可能的话,永远不要使用goto。
然而,在前几天阅读libavcodec(它是用C编写的)时,我惊讶地注意到它的多种用法。
在支持循环和函数的语言中使用goto是否有优势?如果有,为什么?请提供一个具体的例子,清楚地说明使用goto的理由。
当前回答
这些年来,我写了不少汇编语言。最终,每一种高级语言都被编译成gotos。好吧,叫它们“分支”或“跳跃”或其他什么,但它们是gotos。有人能写无goto汇编器吗?
当然,你可以向Fortran、C或BASIC程序员指出,gotos的泛滥就像意大利肉酱面一样。然而,答案不是避免它们,而是小心地使用它们。
刀可以用来准备食物,解救某人,或者杀死某人。我们会因为害怕后者而没有刀吗?同样,“后向”:不小心使用它会碍事,小心使用它会有所帮助。
其他回答
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.
有些人说在c++中没有去的理由。有人说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;
将其与goto-free代码进行比较:
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{…}而看起来更熟悉) 需要额外的循环变量,在四个地方使用 阅读和理解带有循环的工作需要更长的时间 循环不保存任何数据,它只是控制执行的流程,这比简单的标签更难理解
还有一个例子
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本身不应该受到指责。注意,在c++中,label有函数作用域,所以它不会像纯汇编那样污染全局作用域,在纯汇编中,重叠循环有它的位置,而且非常常见——比如下面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:;
我遇到过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是意大利面代码的原因。这个事业就是我们;程序员。如果我愿意,我也可以用函数创建意大利面条代码。宏也是如此。
它有时在按字符进行字符串处理时很方便。
想象一下这样一个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使得程序流的推理变得困难。“意大利面条代码”),goto通常只用于弥补缺失的功能:使用goto实际上可能是可以接受的,但前提是语言没有提供更结构化的变体来获得相同的目标。以《怀疑》为例:
我们使用的goto规则是,goto可以跳转到函数中的单个退出清理点。
这是对的——但前提是语言不允许使用清理代码进行结构化异常处理(如RAII或finally),后者可以更好地完成相同的工作(因为它是专门为此而构建的),或者有很好的理由不使用结构化异常处理(但除非在非常低的级别,否则您永远不会遇到这种情况)。
在大多数其他语言中,goto唯一可接受的用法是退出嵌套循环。即使在这种情况下,将外部循环提升为自己的方法并使用return也总是更好。
除此之外,goto是对特定代码段考虑不够的标志。
支持goto实现一些限制的现代语言(例如,goto可能不会跳转到函数中或跳出函数),但问题从根本上还是一样的。
顺便说一句,其他语言特性当然也是如此,尤其是例外。而且通常有严格的规则,只在指定的地方使用这些特性,例如不使用异常来控制非异常程序流的规则。