Answers:
我知道有一些使用“ goto”语句的原因(有些已经对此进行了说明):
干净地退出功能
通常在函数中,您可能会分配资源,并且需要在多个位置退出。程序员可以通过将资源清除代码放在函数的末尾来简化其代码,并且该函数的所有“退出点”都将进入清除标签。这样,您不必在函数的每个“退出点”都编写清除代码。
退出嵌套循环
如果您处于嵌套循环中并且需要脱离所有循环,那么goto可以比break语句和if-checks更加简洁明了。
低级性能改进
这仅在性能至关重要的代码中有效,但是goto语句执行得非常快,并且在遍历函数时可以助您一臂之力。但是,这是一把双刃剑,因为编译器通常无法优化包含gotos的代码。
请注意,在所有这些示例中,gotos仅限于单个函数的范围。
goto
与return
仅仅是愚蠢的。它不是在“重构”任何东西,而只是在“ goto
重塑”,以便在受到压抑的环境中成长的人(即我们所有人)对使用道德上等于a的东西感觉更好goto
。我更喜欢看环,我用它,看到一点点goto
,这本身是只是一个工具,不是看到有人已经移到环某处无关只是一种逃避goto
。
break
,continue
,return
基本上goto
,只是在漂亮的包装。
do{....}while(0)
除了它在Java中起作用之外,我看不出有什么方法比goto更好。
每个人都goto
直接或间接地反对Edsger Dijkstra的GoTo被认为有害的文章,以证实自己的立场。太糟糕了,Dijkstra的文章实际上与如今使用语句的方式无关goto
,因此,文章所说的内容几乎不适用于现代编程领域。在goto
现在一个宗教稀少米姆的青草,一直到它的经文从高,其高牧师和感知异端的回避(或更糟)决定。
让我们将Dijkstra的论文放在上下文中,以阐明该主题。
当Dijkstra撰写论文时,当时的流行语言是非结构化的过程语言,例如BASIC,FORTRAN(较早的方言)和各种汇编语言。使用高级语言的人们在扭曲的,扭曲的执行线程中跳过整个代码库是很普遍的,从而产生了“意大利面条式代码”一词。您可以跳到迈克·梅菲尔德(Mike Mayfield)编写的经典《迷航(Trek)》游戏中,然后尝试弄清楚事物的工作原理,从而看到这一点。花一点时间看一下。
这是Dijkstra在1968年的论文中所指责的“毫无保留地使用go to statement”。 这是他所居住的环境促使他撰写该论文。他在批评中要求停止在代码中任意位置随意跳转的能力。将其与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()
{
_-_-_-_
_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_
_-_-_-_
}
不在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程序员,但是,我会更密切地关注的用途约50%同行#define
多久,我就反对讨伐去goto
!
因此,对于那些不愿阅读本文的人来说,有几点要注意。
goto
声明是为编程环境下写goto
了一个很大
更多潜在的破坏性比在最现代的语言不属于汇编。goto
和说“我曾经尝试过一次乐趣但不喜欢它,所以现在我反对它”一样合理。goto
语句在代码中有合法用途,无法用其他结构充分替代。godo
“憎恶”,do
它打破了总是错误的循环来break
代替a goto
。这些通常比明智的使用更糟糕goto
。goto
(这是发布的问题)
由于goto
使对程序流的推理变得困难1(又称“意大利面条代码”),goto
通常仅用于补偿缺少的功能:使用goto
可能实际上是可以接受的,但前提是该语言没有提供更结构化的变体来获取相同的目标。以Doubt为例:
我们使用goto的规则是goto可以跳转到函数中的单个出口清理点。
这是正确的-但前提是该语言不允许使用清理代码(例如RAII或finally
)进行结构化异常处理,这样做的效果更好(因为它是专门为此目的而构建的),或者在有充分理由不这样做的情况下使用结构化异常处理(但除非是非常低的级别,否则您永远不会遇到这种情况)。
在大多数其他语言中,唯一可接受的用法goto
是退出嵌套循环。甚至在那里,将外部循环提升为自己的方法并return
改为使用几乎总是更好的选择。
除此之外,这goto
表明没有对特定代码进行足够的思考。
1支持的现代语言goto
实现了一些限制(例如,goto
可能无法跳入或跳出功能),但问题从根本上保持不变。
顺便说一下,其他语言功能当然也是如此,最明显的例外是。而且通常有严格的规则来仅在指示的地方使用这些功能,例如不使用异常来控制非异常程序流的规则。
finally
?因此,将异常用于错误处理之外的其他东西是好的,但使用goto
不好的东西呢?我认为异常的命名很恰当。
好吧,有一件事总是比goto's
;其他程序流运算符的奇怪用法,以避免转到:
例子:
// 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
do{}while(false)
我认为可以认为是惯用的。您不允许不同意:D
goto after_do_block;
而没有实际说出来。否则...只运行一次的“循环”?我称这种滥用控制结构。
#define
s比goto
在C#中, switch语句不允许掉线。因此,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;
}
编辑:“不掉线”规则有一个例外。如果case语句没有代码,则允许穿透。
goto case 5:
在遇到情况1时说出来)。似乎Konrad Rudolph的答案在这里是正确的:goto
正在补偿缺少的功能(并且不如实际功能清晰)。如果我们真正想要的是穿透,则最好的默认设置将是没有穿透,但是continue
要明确要求它。
#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
,它们具有合法用途,但很容易被滥用。例如,在完全非异常的情况下,纯粹为了逃避嵌套的控制结构而抛出异常)
这些年来,我已经编写了多行汇编语言。最终,每种高级语言都可以编译成gotos。好吧,称它们为“分支”或“跳跃”或其他名称,但它们是愚蠢的。任何人都可以编写goto-less汇编程序吗?
现在确定,您可以指出一个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];
}
在您意识到需要更改清除代码之前,这很好。然后,您必须进行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
应始终谨慎使用,并且作为最后的手段-但是有时间和地方。问题不应该是“您是否必须使用它”,而应该是“使用它是否是最佳选择”。
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可以接受的情况,说所有其他用途都不可接受,我觉得很有趣。您是否真的认为您知道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
是一种奖励。
container::iterator it = slot_p.find(hash_key); if (it != slot_p.end()) it->overwrite(hash_key); else it = slot_p.find_first_empty();
我发现这类程序更容易阅读。在这种情况下,每个函数都可以编写为纯函数,这样就更容易推论了。现在,主要功能仅通过功能名称说明代码的功能,然后,如果需要,您可以查看其定义以了解其功能。
我们使用goto的规则是goto可以跳转到函数中的单个出口清理点。在非常复杂的功能中,我们放宽该规则以允许其他跳转。在这两种情况下,我们都避免在错误代码检查中经常出现的嵌套if语句,这有助于提高可读性和可维护性。
goto语句,其合法的用途,和替代结构,可以代替“良性goto语句”中使用,但可能会被滥用一样容易goto语句,是高德纳的文章“最周到和全面的讨论与goto语句结构化编程 ” ,1974年12月的《计算机调查》(第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版本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
最好与其他关键字一起使用。喜欢redo
一些代码:
LABEL: ;
...
goto LABEL if $x;
{
...
redo if $x;
}
或last
从多个位置转到一些代码:
goto LABEL if $x;
...
goto LABEL if $y;
...
LABEL: ;
{
last if $x;
...
last if $y
...
}
我发现do {} while(false)用法完全令人反感。可以想到的是,在某些奇怪的情况下,我有必要说服我这样做,但是从来没有说过这是干净易懂的代码。
如果必须执行这样的循环,为什么不明确声明对flag变量的依赖性?
for (stepfailed=0 ; ! stepfailed ; /*empty*/)
/*empty*/
不是stepfailed = 1
吗?无论如何,这比a更好do{}while(0)
吗?在这两种情况下,您都需要break
退出(或退出stepfailed = 1; continue;
)。对我来说似乎不必要。
1)我所知道的goto最常见的用法是使用不提供它的语言(即C)来模拟异常处理。我会看到这样使用了无数个笨蛋;根据2013年进行的一项快速调查:http://blog.regehr.org/archives/894,Linux代码中大约有100,000个goto 。Linux编码风格指南中甚至提到了Goto用法:https : //www.kernel.org/doc/Documentation/CodingStyle。就像使用填充有函数指针的结构来模拟面向对象的编程一样,goto在C编程中也占有一席之地。那么谁是对的:Dijkstra或Linus(以及所有Linux内核编码器)?基本上是理论与实践。
但是,通常的陷阱是没有编译器级别的支持,也没有检查常见的构造/模式:更容易错误地使用它们并引入错误,而无需进行编译时检查。Windows和Visual C ++但在C模式下通过SEH / VEH提供异常处理,这正是出于这个原因:即使在OOP语言之外(即在过程语言中),异常也很有用。但是,即使编译器为语言中的异常提供了语法支持,也无法始终保存您的培根。以后一种情况为例,著名的Apple SSL“ goto失败”错误仅复制了一个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构造不允许编译器无法轻易找出的目标(例如将goto转换为计算地址(**)),对于使用gotos的函数在编译器中查找无法到达的代码,比使用Dijkstra难得多。批准的代码。
(**)脚注:在某些Basic版本中,可以转到计算的行号,例如GOTO 10 * x,其中x是变量。令人困惑的是,在Fortran中,“ compute goto”是指等效于C中的switch语句的构造。标准C不允许使用该语言中的已计算的goto,而仅允许goto静态/语法声明的标签。但是,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()对函数来模拟非本地出口/异常,但是与其他语言相比,它们具有一些严重的缺点。Wikipedia的文章http://en.wikipedia.org/wiki/Setjmp.h很好地解释了后面的问题。该功能对也可以在Windows(http://msdn.microsoft.com/zh-cn/library/yz2ez4as.aspx)上运行,但是几乎没有人在此使用它们,因为SEH / VEH更为出色。即使在Unix上,我也很少使用setjmp和longjmp。
2)我认为C语言中goto的第二个最常见用法是实现多级中断或多级继续,这也是一个毫无争议的用例。回想一下Java不允许goto标签,但允许break标签或Continue标签。根据http://www.oracle.com/technetwork/java/simple-142616.html,这实际上是C语言中gotos的最常见用例(他们说90%),但是根据我的主观经验,系统代码倾向于经常使用gotos进行错误处理。也许在科学代码中,或者在OS提供异常处理(Windows)的地方,多级退出是主要的用例。他们实际上没有提供有关调查背景的任何细节。
编辑添加:事实证明,这两种使用模式在第60页左右的Kernighan和Ritchie的C书中找到(取决于版本)。值得注意的另一件事是,两个用例都只涉及正向指令。事实证明,MISRA C 2012版(与2004版不同)现在允许gotos,只要它们只是向前的即可。
有人说在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?
}
}
现在让我们摆脱“邪恶的” 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本身不应该受到指责。请注意,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”和“ goto-less编程”运动的最重要论据的问题是,如果使用得太频繁,您的代码虽然可能会正确运行,但变得难以阅读,无法维护,无法查看等。在99.99%的情况“转到”会导致意大利面条式代码。就我个人而言,我无法想到有什么很好的理由来说明为什么要使用“ goto”。
goto
)的成本可能是一个有效的论点。@cschol的用法类似:虽然可能现在不设计语言,但他基本上是在评估设计师的工作。
goto
变量存在的环境中使用语言允许比尝试支持某人可能需要的每种控制结构便宜。用编写代码goto
可能不如使用其他结构好,但是使用编写此类代码goto
将有助于避免“表现力方面的漏洞”,即一种语言无法编写有效代码的构造。
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。为什么?
原因是因为从程序的另一个角度来看,我可以插入a GOTO FINAL
而无需控制A的值。
这个例子非常明显,但是随着程序变得越来越复杂,看到这类东西的难度也会增加。
有关材料可以在迪克斯特拉先生的著名文章“反对GO声明的案例”中找到。
在以下情况下,我将使用goto:在需要从不同位置的函数返回时,以及在返回之前,需要完成一些未初始化的操作:
非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;
}
当您需要更改释放语句中的某些内容时,第二个版本使操作变得更容易(每个代码在代码中使用一次),并减少了在添加新分支时跳过其中任何一条的机会。将它们移动到函数中将无济于事,因为可以在不同的“级别”进行取消分配。
finally
在C#中有块
finally
)。或者,使用goto
s,但使用一个公共出口点,该出口点始终进行所有清理。但是,每种清除方法都可以处理为null或已经清除的值,或者受条件测试保护,因此在不适当的情况下可以跳过。
goto
s都到达相同的出口点,该出口点具有相同的逻辑(如您所说,如果每个资源都需要一个额外的逻辑)。但是没关系,当使用C
正确的代码时-不管代码在C语言中的原因为何,几乎可以肯定的是,这种折衷选择了最“直接”的代码。(我的建议处理的是可能分配给定资源或可能未分配给任何资源的复杂情况。但是,是的,在这种情况下过度使用了。)
在该领域做出了重大贡献的计算机科学家Edsger Dijkstra也因批评GoTo的使用而闻名。关于Wikipedia的论点有一篇简短的文章。
它对于不时进行字符字符串处理非常有用。
试想像这样的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
为函数调用,但是如果要修改几个不同的局部变量,则除非您的语言支持可变闭包,否则您必须传递对每个局部变量的引用。continue
如果逻辑使其他情况不起作用,则在调用后仍必须使用语句(可以说是goto的一种形式)以获取相同的语义。
在词法分析器中,我也明智地使用了goto,通常用于类似情况。大多数时候您不需要它们,但是对于那些奇怪的情况,它们很好。