Lan*_*don 194 c language-agnostic exception-handling goto
我长期以来一直认为,goto
如果可能的话,永远不应该使用它.在前几天阅读libavcodec(用C语言编写)时,我注意到它的多种用途.goto
在支持循环和函数的语言中使用是否有利?如果是这样,为什么?
JUS*_*ION 896
每个人都goto
直接或间接地反驳了Edsger Dijkstra的GoTo Considered Harmful文章,以证实他们的立场.太糟糕了Dijkstra的文章几乎与当前使用语句的方式无关goto
,因此文章所说的对现代编程场景几乎没有适用性.该goto
稀少梅梅现在近乎一种宗教,一直到它的经文从上决定的,它的大祭司和感知异端的回避(或更糟).
让我们把Dijkstra的论文放到上下文中,以便对这个主题有所了解.
当Dijkstra写他的论文时,当时流行的语言是非结构化的程序,如BASIC,FORTRAN(早期的方言)和各种汇编语言.使用高级语言的人在扭曲的,扭曲的执行线程中跳过他们的代码库是很常见的,这导致术语"意大利面条代码".你可以通过跳过迈克梅菲尔德写的经典Trek游戏并试图找出工作原理来看到这一点.花点时间看一下.
这是Dijkstra在1968年的论文中反对的"肆无忌惮地使用声明". 这就是他所生活的环境,这使他撰写了这篇论文.能够在您喜欢的任何时候在您的代码中随意跳转,这是他批评和要求停止的内容.将其与goto
C语言或其他此类更现代语言中的贫血力量进行比较,简直是可笑的.
当他们面对异教徒时,我已经可以听到邪教徒的颂歌了."但是,"他们会唱歌,"你可以用goto
C语言编写代码非常困难".哦耶?你可以在没有它的情况下使代码难以阅读goto
.像这个:
#define _ -F<00||--F-OO--;
int F=00,OO=00;main(){F_OO();printf("%1.3f\n",4.*-F/OO/OO);}F_OO()
{
_-_-_-_
_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_
_-_-_-_
}
Run Code Online (Sandbox Code Playgroud)
不是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*/
Run Code Online (Sandbox Code Playgroud)
不goto
存在要么.因此它必须是可读的.
这些例子我的观点是什么?这不是语言功能,使得代码难以理解,无法维护.这不是语法.造成这种情况的是程序员.正如您在上面的项目中看到的那样,糟糕的程序员可以使任何语言功能都不可读和无法使用.就像for
那里的循环一样.(你可以看到他们,对吧?)
现在公平地说,一些语言结构比其他语言结构更容易被滥用.但是,如果你是一名C程序员,那么#define
在我开始讨伐之前,我会更加密切地关注大约50%的用途goto
!
因此,对于那些一直困扰着阅读的人来说,有几点需要注意.
goto
声明是为编程环境下写goto
了一个很大
更多潜在的破坏性比在最现代的语言不属于汇编.goto
由于这种情况,自动丢弃所有用途,就像说"我试过一次但是不喜欢它,所以现在我反对它"一样理性.goto
代码中现代(贫血)陈述的合法用法无法被其他构造充分替代.godo
"憎恶",其中一个总是错误的do
循环被break
用来代替a goto
.这些往往比明智的使用更糟糕goto
.请记住,当你以一个接一个的方式投票给我时,我goto
在过去的15到20年中已经在我自己的(非汇编)代码中使用了3次.
我等待着愤怒的尖叫声和-1票,屏住呼吸.
Chr*_*lum 233
使用我所知道的"goto"语句有几个原因(有些人已经说过了):
干净地退出功能
通常在函数中,您可以分配资源并需要在多个位置退出.程序员可以通过将资源清理代码放在函数末尾来简化代码,函数的所有"退出点"都会转到清理标签.这样,您就不必在函数的每个"出口点"编写清理代码.
退出嵌套循环
如果你在一个嵌套循环中并且需要打破所有循环,那么goto可以使它比break语句和if-checks更清晰,更简单.
低级别的性能改进
这仅在perf-critical代码中有效,但goto语句执行速度非常快,并且在移动函数时可以提升.然而,这是一把双刃剑,因为编译器通常无法优化包含gotos的代码.
请注意,在所有这些示例中,gotos仅限于单个函数的范围.
dsi*_*cha 151
盲目地遵守最佳做法并非最佳做法.避免将goto
语句作为流量控制的主要形式的想法是避免产生不可读的意大利面条代码.如果在适当的地方谨慎使用,它们有时可能是表达想法的最简单,最清晰的方式.Zortech C++编译器和D编程语言的创建者Walter Bright经常使用它们,但是明智地使用它们.即使有这些goto
陈述,他的代码仍然完全可读.
底线:避免goto
避免goto
是没有意义的.你真正想要避免的是生成不可读的代码.如果您的goto
-laden代码是可读的,那么它没有任何问题.
Kon*_*lph 36
由于goto
对程序流程的推理很难1(又称"意大利面条代码"),goto
通常只用于弥补缺失的特征:goto
实际使用可能是可以接受的,但只有当语言不提供更结构化的变体才能获得同样的目标.以怀疑为例:
我们使用goto的规则是goto可以跳转到函数中的单个退出清理点.
这是正确的 - 但是只有当语言不允许使用清理代码(例如RAII或finally
)进行结构化异常处理时,它才能更好地完成相同的工作(因为它是专门为执行它而构建的),或者当有充分的理由不这样做时采用结构化异常处理(但除非处于非常低的水平,否则你永远不会有这种情况).
在大多数其他语言中,唯一可接受的用途goto
是退出嵌套循环.即使在那里,将外环提升为自己的方法并使用它也几乎总是更好return
.
除此之外,这goto
是一个迹象,表明在特定的代码段中没有进行足够的考虑.
1支持goto
实现某些限制的现代语言(例如,goto
可能不会跳入或跳出功能)但问题从根本上保持不变.
顺便提一下,对于其他语言功能当然也是如此,最明显的例外.并且通常存在严格的规则,仅在指定时使用这些功能,例如不使用异常来控制非例外程序流的规则.
Vik*_*ehr 35
嗯,有一件事总是比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
Run Code Online (Sandbox Code Playgroud)
Jak*_*urc 28
在C# switch语句中,doest不允许掉线.因此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;
}
Run Code Online (Sandbox Code Playgroud)
编辑:"没有通过"规则有一个例外.如果case语句没有代码,则允许直通.
Chr*_*ung 14
#ifdef TONGUE_IN_CHEEK
Perl有一个goto
允许你实现穷人的尾调用.:-P
sub factorial {
my ($n, $acc) = (@_, 1);
return $acc if $n < 1;
@_ = ($n - 1, $acc * $n);
goto &factorial;
}
Run Code Online (Sandbox Code Playgroud)
#endif
好的,这与C无关goto
.更严重的是,我同意其他关于使用goto
清理,或实现Duff设备等的评论.这都是关于使用,而不是滥用.
(相同的注释可以适用于longjmp
,例外call/cc
等等 - 它们具有合法用途,但很容易被滥用.例如,在完全非特殊情况下,抛出异常纯粹是为了逃避深层嵌套的控制结构.)
bug*_*net 12
多年来,我写了几行汇编语言.最终,每种高级语言都会编译为getos.好吧,称他们为"分支"或"跳跃"或其他任何东西,但他们是必须的.任何人都可以写无转换汇编程序吗?
现在可以肯定的是,你可以向Fortran,C或BASIC程序员指出,与gotos一起进行骚乱是意大利面食的一个秘方.答案不是要避免它们,而是要小心使用它们.
刀可用于准备食物,释放某人或杀死某人.我们害怕后者吗?同样地,goto:不经意地使用它会阻碍,小心使用它会有所帮助.
虽然使用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];
}
Run Code Online (Sandbox Code Playgroud)
在您意识到需要更改清理代码之前,这很好.然后你必须经历并进行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;
}
Run Code Online (Sandbox Code Playgroud)
这样做的好处是,您的代码可以访问执行清理所需的所有内容,并且您已设法大大减少了更改点的数量.另一个好处是你已经从你的功能的多个出口点变成了一个; 如果没有清理,你就不会意外地从功能中返回.
此外,由于goto仅用于跳转到单个点,因此并不是说您要创建大量的意大利面条代码来回尝试模拟函数调用.相反,goto实际上有助于编写更结构化的代码.
总之,goto
应该谨慎使用,并作为最后的手段 - 但它有一个时间和地点.问题不应该是"你必须使用它",而是"它是使用它的最佳选择".
其中一个原因是goto很糟糕,除了编码风格之外,你可以用它来创建重叠但非嵌套的循环:
loop1:
a
loop2:
b
if(cond1) goto loop1
c
if(cond2) goto loop2
Run Code Online (Sandbox Code Playgroud)
这将创建奇怪但可能合法的控制流结构,其中像(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
Run Code Online (Sandbox Code Playgroud)
现在,如果我没有使用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
Run Code Online (Sandbox Code Playgroud)
如果添加更多传递,它会变得越来越糟,而带有goto的版本始终保持相同的缩进级别,并且避免使用伪结果if语句,其结果由前一个循环的执行所暗示.
所以还有另一种情况,goto使代码更清晰,更容易编写和理解......我确信还有更多,所以不要假装知道goto有用的所有情况,解散你不能的任何好的没想到.
有人说 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;
Run Code Online (Sandbox Code Playgroud)
将其与无 goto 的代码进行比较:
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;
Run Code Online (Sandbox Code Playgroud)
我看到这些差异:
{}
需要嵌套块(尽管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?
}
}
Run Code Online (Sandbox Code Playgroud)
现在让我们摆脱“邪恶”的 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);
}
Run Code Online (Sandbox Code Playgroud)
您会看到它与使用 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
}
}
Run Code Online (Sandbox Code Playgroud)
关键是 goto 很容易被误用,但这不能归咎于 goto 本身。请注意,标签在 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
Run Code Online (Sandbox Code Playgroud)
还有另一个优点: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)
Run Code Online (Sandbox Code Playgroud)
或者您可以使用带有缩进的等效 goto,因此如果您明智地选择标签名称,则不需要注释:
if(!valid) goto NOTVALID;
LOOPBACK:
// more than one page of code here
if(loop) goto LOOPBACK;
NOTVALID:;
Run Code Online (Sandbox Code Playgroud)
我们使用goto的规则是goto可以跳转到函数中的单个退出清理点.在非常复杂的功能中,我们放宽了规则以允许其他跳跃前进.在这两种情况下,我们都避免使用经常出现错误代码检查的深层嵌套if语句,这有助于提高可读性和维护性.
小智 5
goto语句,其合法的用途,和替代结构,可以代替“良性goto语句”中使用,但可能会被滥用一样容易goto语句,是高德纳的文章“最周到和全面的讨论与goto语句结构化编程 ” ,在1974年12月的计算机调查中(第6卷第4期,第261-301页)。
毫不奇怪,这份已有39年历史的论文的某些方面是过时的:处理能力的数量级增加使得Knuth的性能改进在中等大小的问题上不明显,并且从那时起发明了新的编程语言结构。(例如,try-catch块包含Zahn的Construct,尽管很少以这种方式使用。)但是Knuth涵盖了论点的方方面面,在任何人再次讨论该问题之前,都应阅读该书。
小智 5
我遇到过一种情况,agoto
是一个很好的解决方案,但我在这里或任何地方都没有看到这个例子。
我有一个开关案例,其中有几个案例最终都需要调用相同的函数。我还有其他情况,最终都需要调用不同的函数。
这看起来有点像这样:
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 ;
}
Run Code Online (Sandbox Code Playgroud)
我没有给每个案例一个函数调用,而是break
用goto
. 跳转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 ;
}
Run Code Online (Sandbox Code Playgroud)
情况 1 到 3 都必须调用doStuffFor123()
,同样情况 4 到 6 也必须调用,doStuffFor456()
依此类推。
在我看来,如果你正确使用 goto 就完全没问题。最后,任何代码都像人们写的那样清晰。使用 goto 可以编写意大利面条式代码,但这并不意味着 goto 是意大利面条式代码的原因。这个原因就是我们;程序员。如果我愿意,我还可以使用函数创建意大利面条式代码。对于宏来说也是如此。