我一直认为,如果可能,永远不应该使用goto
。在前几天阅读libavcodec(用C语言编写)时,我注意到它的多种用途。在支持循环和函数的语言中使用goto
是否有利?如果是这样,为什么?
使用我所知道的“goto”语句有几个原因(有些人已经说过了):
干净地退出功能
通常在函数中,您可以分配资源并需要在多个位置退出。程序员可以通过将资源清理代码放在函数末尾来简化代码,函数的所有“退出点”都会转到清理标签。这样,您就不必在函数的每个“出口点”编写清理代码。
退出嵌套循环
如果你在一个嵌套循环中并且需要打破所有循环,那么goto可以使它比break语句和if-checks更清晰,更简单。
低级别的性能改进
这仅在perf-critical代码中有效,但goto语句执行速度非常快,并且在移动函数时可以提升。然而,这是一把双刃剑,因为编译器通常无法优化包含gotos的代码。
请注意,在所有这些示例中,gotos仅限于单个函数的范围。
其中一个原因是goto很糟糕,除了编码风格之外,你可以用它来创建重叠但非嵌套的循环:
loop1:
a
loop2:
b
if(cond1) goto loop1
c
if(cond2) goto loop2
这将创建奇怪但可能合法的控制流结构,其中像(a,b,c,b,a,b,a,b,...)这样的序列是可能的,这使得编译器黑客不高兴。显然,有许多聪明的优化技巧依赖于这种未发生的结构。 (我应该检查我的龙书副本......)这可能(使用一些编译器)的结果是对于包含goto
s的代码没有进行其他优化。
如果您只是知道它,“哦,顺便说一下”,恰好可以说服编译器发出更快的代码。就个人而言,我更愿意尝试向编译器解释在使用像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的规则是goto可以跳转到函数中的单个退出清理点。在非常复杂的功能中,我们放宽了规则以允许其他跳跃前进。在这两种情况下,我们都避免使用经常出现错误代码检查的深层嵌套if语句,这有助于提高可读性和维护性。
关于goto语句,它们的合法用法以及替代构造的最深思熟虑和彻底的讨论,可以用来代替“良性goto语句”,但可以像goto语句一样容易被滥用,是Donald Knuth的文章“Structured Programming with goto Statements”,12月1974年计算机调查(第6卷,第4期,第261 - 301页)。
毫不奇怪,这篇有39年历史的论文的某些方面已经过时了:处理能力的数量级增加使得Knuth的一些性能改进对于中等大小的问题不明显,并且从那时起就发明了新的编程语言结构。 (例如,try-catch块包含Zahn的Construct,虽然它们很少以这种方式使用。)但是Knuth涵盖了论证的所有方面,并且应该在任何人再次重新解决问题之前阅读。
在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 version 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
are better done with other keywords.像redo
ing一些代码:
LABEL: ;
...
goto LABEL if $x;
{
...
redo if $x;
}
或者从多个地方转到last
的一些代码:
goto LABEL if $x;
...
goto LABEL if $y;
...
LABEL: ;
{
last if $x;
...
last if $y
...
}
如果是这样,为什么?
C没有多级/标记中断,并且并非所有控制流都可以使用C的迭代和决策原语轻松建模。我们在解决这些缺陷方面走了很长的路。
有时使用某种类型的标志变量来实现一种伪多级别中断更为清晰,但它并不总是优于goto(至少goto允许人们轻松确定控制的位置,与标志变量不同) ),有时你根本不想支付旗帜/其他扭曲的性能价格来避免goto。
libavcodec是一个性能敏感的代码片段。直接表达控制流可能是一个优先事项,因为它往往会更好地运行。
同样没有人实施过“来自”声明....
我发现do {} while(false)用法完全令人反感。可以想象可能会说服我在某些奇怪的情况下是必要的,但从来没有说它是干净的合理代码。
如果你必须做一些这样的循环,为什么不明确依赖于flag变量呢?
for (stepfailed=0 ; ! stepfailed ; /*empty*/)
1)我所知道的goto最常见的用法是在不提供它的语言中模拟异常处理,即在C语言中。(上面给出的核代码就是这样。)看看Linux源代码,你呢?我会看到那种方式使用的bazillion gotos;根据2013年进行的一项快速调查显示,Linux代码中有大约100,000个getos:http://blog.regehr.org/archives/894。在Linux编码风格指南中甚至提到了Goto用法:https://www.kernel.org/doc/Documentation/CodingStyle。就像使用填充了函数指针的结构模拟面向对象编程一样,goto在C编程中占有一席之地。那么谁是对的:Dijkstra或Linus(以及所有Linux内核程序员)?它基本上是理论与实践。
然而,通常的问题是没有编译器级别的支持和检查常见的构造/模式:它更容易使用它们错误并引入错误而没有编译时检查。 Windows和Visual C ++但在C模式下通过SEH / VEH提供异常处理,原因如下:即使在OOP语言之外,例如在过程语言中,异常也是有用的。但编译器无法始终保存您的培根,即使它为语言中的异常提供语法支持。考虑后一种情况的例子,着名的Apple SSL“goto fail”错误,它只是重复了一个带有灾难性后果的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构造不允许编译器无法轻易找到的目标,比如计算地址(**),编译器在使用gotos的函数内找到无法访问的代码并不比使用Dijkstra更困难。批准的代码。
(**)脚注:Gotos可以在某些版本的Basic中计算行号,例如: GOTO 10 * x其中x是变量。相当容易混淆,在Fortran中“计算goto”指的是一个等同于C语言中的switch语句的构造。标准C不允许在语言中使用计算的gotos,但只允许使用静态/语法声明的标签。但是,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()函数对,但与其他语言相比,它们有一些严重的缺点。维基百科的文章http://en.wikipedia.org/wiki/Setjmp.h很好地解释了后一个问题。这个功能对也适用于Windows(http://msdn.microsoft.com/en-us/library/yz2ez4as.aspx),但几乎没有人在那里使用它们,因为SEH / VEH是优越的。即使在Unix上,我认为setjmp和longjmp很少使用。
2)我认为在C中goto的第二个最常见的用途是实现多级中断或多级继续,这也是一个相当无争议的用例。回想一下,Java不允许使用goto标签,但允许使用break标签或继续标签。根据http://www.oracle.com/technetwork/java/simple-142616.html的说法,这实际上是C语言中最常见的使用案例(他们说90%),但在我的主观经验中,系统代码更倾向于使用gotos进行错误处理。也许在科学代码中或OS提供异常处理(Windows)的情况下,多级出口是主要的用例。他们并没有真正详细说明他们的调查背景。
编辑补充说:事实证明,这两种使用模式可以在第60页的Kernighan和Ritchie的C书中找到(取决于版本)。另一件需要注意的是,两个用例都只涉及前向的getos。事实证明,MISRA C 2012版(不像2004版)现在允许玩家,只要他们只是前锋。
有人说没有理由在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?
}
}
现在让我们摆脱“邪恶”的转到:
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的相同类型,它是结构良好的模式,它不是前转,因为许多推广作为唯一推荐的方式。当然你想要避免这样的“智能”代码:
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:;
每个反goto
的人都直接或间接引用Edsger Dijkstra的GoTo Considered Harmful文章来证实他们的立场。太糟糕了Dijkstra的文章几乎与goto
语句的使用方式无关,因此文章所说的对现代编程场景几乎没有适用性。 goto
-less meme现在接近一个宗教,直到它的高级,高级牧师和回避(或更糟)的异教徒的经文。
让我们把Dijkstra的论文放到上下文中,以便对这个主题有所了解。
当Dijkstra写他的论文时,当时流行的语言是非结构化的程序,如BASIC,FORTRAN(早期的方言)和各种汇编语言。使用高级语言的人在扭曲的,扭曲的执行线程中跳过他们的代码库是很常见的,这导致术语“意大利面条代码”。你可以通过跳过迈克梅菲尔德写的the classic Trek game并试图找出工作原理来看到这一点。花点时间看一下。
这是Dijkstra在1968年的论文中反对的“肆无忌惮地使用声明”。这就是他所生活的环境,这使他撰写了这篇论文。能够在您喜欢的任何时候在您的代码中随意跳转,这是他批评并要求停止的。将其与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程序员,那么在我对#define
进行讨伐之前很久,我就会更加密切地关注goto
约50%的用途!
因此,对于那些一直困扰着阅读的人来说,有几点需要注意。
goto
语句的论文是为编程环境而写的,其中goto
比大多数不是汇编程序的现代语言更具潜在破坏性。goto
的所有用途,就像说“我试过一次但是不喜欢它,所以现在我反对它”一样理性。goto
语句的合法用法,这些语句不能被其他结构充分替代。godo
”憎恶,其中一个假的do
循环被打破使用break
代替goto
。这些往往比明智地使用goto
更糟糕。在Perl中,使用标签从循环中“转到” - 使用“last”语句,类似于break。
这样可以更好地控制嵌套循环。
传统的goto标签也受支持,但我不确定有太多的实例,这是实现你想要的唯一方法 - 子程序和循环应该足以满足大多数情况。
'goto'的问题和'goto-less programming'运动最重要的论点是,如果你过于频繁地使用它,你的代码虽然可能表现正常,但却变得不可读,不可维护,不可查看等。在99.99%的案件'goto'导致意大利面条代码。就个人而言,我想不出有什么理由可以使用'goto'。
当然,可以使用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.为什么?
原因是因为从程序的另一点我可以插入一个GOTO FINAL
而不控制A的值。
这个例子非常明显,但随着程序变得越来越复杂,看到这类事情的难度也越来越大。
相关资料可以在Dijkstra "A case against the GO TO statement"先生的着名文章中找到
我在以下情况下使用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;
}
当您需要在释放语句中更改某些内容(每个在代码中使用一次)时,第二个版本会更容易,并且在添加新分支时减少跳过其中任何内容的机会。在函数中移动它们在这里没有用,因为释放可以在不同的“级别”完成。
Edsger Dijkstra是一位在该领域做出重大贡献的计算机科学家,他也因批评使用GoTo而闻名。有一篇关于他关于Wikipedia的论点的短文。
它可以随时用于字符串处理。
想象一下这个printf-esque例子:
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形式)来获得相同的语义。
我也在词法分析器中明智地使用了gotos,通常用于类似的情况。你大部分时间都不需要它们,但是对于那些奇怪的情况它们很好。
盲目地遵守最佳做法并非最佳做法。避免使用goto
语句作为流量控制的主要形式的想法是避免产生不可读的意大利面条代码。如果在适当的地方谨慎使用,它们有时可能是表达想法的最简单,最清晰的方式。 Zortech C ++编译器和D编程语言的创建者Walter Bright经常使用它们,但是明智地使用它们。即使使用goto
语句,他的代码仍然完全可读。
底线:避免使用goto
以避免goto
是没有意义的。你真正想要避免的是生成不可读的代码。如果你的goto
-laden代码是可读的,那么它没有任何问题。
由于goto
对程序流程hard1(又称“意大利面条代码”)进行推理,因此goto
通常仅用于补偿缺失的特征:使用goto
实际上可能是可以接受的,但前提是该语言不提供更结构化的变体获得相同的目标。以怀疑为例:
我们使用goto的规则是goto可以跳转到函数中的单个退出清理点。
这是真的 - 但是只有当语言不允许使用清理代码(例如RAII或finally
)进行结构化异常处理时,它才能更好地完成相同的工作(因为它是专门为这样做而构建的),或者当有充分的理由时不采用结构化异常处理(但除非处于非常低的水平,否则你永远不会遇到这种情况)。
在大多数其他语言中,goto
唯一可接受的用途是退出嵌套循环。即使在那里,将外环提升为自己的方法并使用return
几乎总是更好。
除此之外,goto
是一个标志,没有足够的想法进入特定的代码。
1支持goto
的现代语言实现了一些限制(例如goto
可能不会跳入或跳出函数)但问题基本上保持不变。
顺便提一下,对于其他语言功能当然也是如此,最明显的例外。并且通常存在严格的规则,仅在指定时使用这些功能,例如不使用异常来控制非例外程序流的规则。
好吧,有一件事总是比goto's
更糟糕;奇怪的是使用其他程序流操作符来避免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
在C#switch声明doest not allow fall-through。所以goto用于将控制转移到特定的开关盒标签或default标签。
例如:
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语句没有代码,则允许直通。
#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's device等的评论。这都是关于使用,而不是滥用。
(相同的评论可以应用于longjmp
,例外,call/cc
等 - 它们具有合法用途,但很容易被滥用。例如,抛出异常纯粹是为了逃避深层嵌套的控制结构,完全不是特殊情况下。)
多年来,我写了几行汇编语言。最终,每种高级语言都会编译为getos。好吧,称他们为“分支”或“跳跃”或其他任何东西,但他们是必须的。任何人都可以写无转换汇编程序吗?
现在可以肯定的是,你可以向Fortran,C或BASIC程序员指出,与gotos一起进行骚乱是意大利面食的一个秘方。答案不是要避免它们,而是要小心使用它们。
刀可用于准备食物,释放某人或杀死某人。因害怕后者,我们没有刀吗?同样地,goto:不经意地使用它会阻碍,小心使用它会有所帮助。
看看When To Use Goto When Programming in C:
虽然使用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
应该总是谨慎使用,并作为最后的手段 - 但它有一个时间和地点。问题不应该是“你必须使用它”,而是“它是使用它的最佳选择”。