在支持循环和函数的语言中使用“goto”是否有利?如果是这样,为什么?
-
09-06-2019 - |
题
长期以来我一直有一个印象 goto
如果可能的话,绝对不应该使用。前几天仔细阅读 libavcodec(用 C 语言编写)时,我注意到它的多种用途。使用是否有利 goto
使用支持循环和函数的语言?如果是这样,为什么?
解决方案
据我所知,使用“goto”语句有几个原因(有些人已经谈到过这一点):
干净地退出函数
通常在一个函数中,您可能会分配资源并需要在多个地方退出。程序员可以通过将资源清理代码放在函数末尾来简化代码,并且函数的所有“退出点”都将转到清理标签。这样,您就不必在函数的每个“退出点”编写清理代码。
退出嵌套循环
如果您处于嵌套循环中并且需要跳出 全部 循环中,goto 可以使这比break语句和if检查更干净、更简单。
低级性能改进
这仅在性能关键的代码中有效,但 goto 语句执行速度非常快,并且可以在移动函数时给您带来帮助。然而,这是一把双刃剑,因为编译器通常无法优化包含 goto 的代码。
请注意,在所有这些示例中,goto 仅限于单个函数的范围。
其他提示
所有反对的人goto
直接或间接引用 Edsger Dijkstra 的 GoTo 被认为有害 文章来证实他们的立场。太糟糕了 Dijkstra 的文章实际上已经 没有什么 与方式有关 goto
现在已经使用了语句,因此本文所说的内容几乎不适用于现代编程场景。这 goto
-更少的模因现在接近一种宗教,一直到它的经文是由上层口授的,它的大祭司和回避(或更糟)被认为是异端的人。
让我们将 Dijkstra 的论文放在上下文中,以阐明这个主题。
当 Dijkstra 撰写论文时,当时流行的语言是非结构化的过程语言,如 BASIC、FORTRAN(早期的方言)和各种汇编语言。对于使用高级语言的人来说跳转是很常见的 遍布他们的代码库 在扭曲的执行线程中,产生了术语“意大利面条代码”。您可以通过跳到来看到这一点 经典的迷航游戏 由迈克·梅菲尔德(Mike Mayfield)撰写,试图弄清楚事情是如何运作的。花点时间看一下。
这 Dijkstra 在 1968 年的论文中谴责了“肆无忌惮地使用 go to 语句”。 这 是他生活的环境促使他写了那篇论文。他批评并要求停止这种在代码中任意位置跳转的能力。与贫血的力量相比 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
!
因此,对于那些费心阅读本文的人来说,有几个要点需要注意。
- Dijkstra 的论文
goto
语句是为编程环境编写的,其中goto
曾经是一个 很多比大多数非汇编语言的潜在破坏性更大。 - 自动丢弃所有使用
goto
因此,与说“我试图玩得开心,但不喜欢它,所以现在我反对它一样理性”。 - 现代(贫血)有合法用途
goto
代码中无法充分替代其他构造的语句。 - 当然,同样的陈述也存在非法使用的情况。
- 也存在对现代控制语句的非法使用,例如“
godo
“令人憎恶的地方总是虚假的do
循环被打破使用break
代替一个goto
. 。这些往往比明智地使用更糟糕goto
.
盲目遵循最佳实践并不是最佳实践。回避的想法 goto
语句作为流程控制的主要形式是为了避免产生不可读的意大利面条式代码。如果在正确的地方谨慎使用,它们有时可能是表达想法的最简单、最清晰的方式。Walter Bright,Zortech C++ 编译器和 D 编程语言的创建者,经常但明智地使用它们。即使与 goto
声明,他的代码仍然完全可读。
底线:避免 goto
为了避免 goto
毫无意义。您真正想要避免的是生成不可读的代码。如果你的 goto
-laden 代码是可读的,那么它就没有问题。
自从 goto
使得程序流程的推理变得困难1 (又名。“意大利面条代码”), 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(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
用于清理或实施 达夫的装置, ,或类似的。一切都是为了使用,而不是滥用。
(同样的评论也适用于 longjmp
, 、例外情况、 call/cc
, 等——它们有合法用途,但很容易被滥用。例如,在完全非异常的情况下,抛出异常纯粹是为了逃避深层嵌套的控制结构。)
多年来我已经编写了不止几行汇编语言。最终,每种高级语言都会编译为 goto。好吧,称它们为“分支”或“跳跃”或其他什么名字,但它们是 goto。任何人都可以编写无 goto 汇编程序吗?
现在,当然,您可以向 Fortran、C 或 BASIC 程序员指出,使用 goto 进行混乱是意大利肉酱面的秘诀。然而,答案不是避免它们,而是谨慎使用它们。
刀可以用来准备食物、释放某人或杀死某人。我们是否因为害怕后者而没有刀子?同样的转到:用之不慎则有碍,用之慎则有助。
看一眼 C 编程时何时使用 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 是表达算法的最佳选择的每种情况吗?
为了说明这一点,我将举一个这里还没有人展示过的例子:
今天我正在编写在哈希表中插入元素的代码。哈希表是以前计算的缓存,可以随意覆盖(影响性能但不影响正确性)。
哈希表的每个桶都有 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 语句,这有助于可读性和可维护性。
Donald Knuth 的文章对 goto 语句、其合法用途以及可以用来代替“良性 goto 语句”但可能像 goto 语句一样容易被滥用的替代结构进行了最深思熟虑和彻底的讨论。使用 goto 语句进行结构化编程”,载于 1974 年 12 月的《计算机调查》(第 6 卷,第 1 期)。4.页数261 - 301)。
毫不奇怪,这篇已有 39 年历史的论文的某些内容已注明日期:处理能力的数量级增长使得 Knuth 的一些性能改进对于中等规模的问题来说并不明显,从那时起,新的编程语言结构就被发明了。(例如,try-catch 块包含 Zahn 的构造,尽管它们很少以这种方式使用。)但是 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
...
}
如果是这样,为什么?
C 没有多级/标记中断,并且并非所有控制流都可以使用 C 的迭代和决策原语轻松建模。goto 在纠正这些缺陷方面大有帮助。
有时,使用某种标志变量来实现一种伪多级中断会更清楚,但它并不总是优于 goto(至少 goto 允许人们轻松确定控制权的去向,与标志变量不同) ),有时您根本不想付出标志/其他扭曲的性能代价来避免跳转。
libavcodec 是一段对性能敏感的代码。控制流的直接表达可能是一个优先事项,因为它往往会运行得更好。
还好没有人执行过“COME FROM”声明......
我发现 do{} while(false) 的用法非常令人反感。可以想象,这可能会让我相信在某些奇怪的情况下这是必要的,但绝不是它是干净合理的代码。
如果您必须执行一些这样的循环,为什么不明确对标志变量的依赖呢?
for (stepfailed=0 ; ! stepfailed ; /*empty*/)
当然可以使用 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的值。
这个例子非常明显,但是随着程序变得越来越复杂,看到这些事情的难度也随之增加。
相关资料可以参见先生的著名文章。迪克斯特拉 “反对 GO TO 声明的案例”
1) 据我所知,goto 最常见的用途是在不提供它的语言(即 C 语言)中模拟异常处理。(上面 Nuclear 给出的代码就是这样。)查看 Linux 源代码,您会看到无数的 goto 都是这样使用的;根据 2013 年进行的一项快速调查,Linux 代码中大约有 100,000 个 goto: 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失败”错误,它只是重复了一个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 到计算地址(**),编译器在使用 goto 的函数内查找无法访问的代码并不比使用 Dijkstra 更难- 批准的代码。
(**) 脚注:在 Basic 的某些版本中可以转到计算的行号,例如GOTO 10*x 其中 x 是变量。相当令人困惑的是,在 Fortran 中,“计算 goto”指的是相当于 C 中 switch 语句的构造。标准 C 语言不允许计算 goto,而只允许 goto 静态/语法声明的标签。然而,GNU C 有一个扩展来获取标签的地址(一元前缀 && 运算符),并且还允许转到 void* 类型的变量。看 https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html 有关这个晦涩的子主题的更多信息。本文的其余部分不涉及那个晦涩的 GNU C 功能。
标准 C(即未计算) goto 通常不是编译时无法找到无法访问的代码的原因。通常的原因是如下逻辑代码。给定
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)我认为 goto 在 C 中的第二个最常见用途是实现多级中断或多级继续,这也是一个相当没有争议的用例。回想一下,Java 不允许 goto 标签,但允许 break 标签或 continue 标签。根据 http://www.oracle.com/technetwork/java/simple-142616.html, ,这实际上是 C 语言中 gotos 最常见的用例(他们说 90%),但根据我的主观经验,系统代码倾向于更频繁地使用 gotos 进行错误处理。也许在科学代码或操作系统提供异常处理(Windows)的地方,多级退出是主要用例。他们并没有真正提供有关调查背景的任何细节。
编辑添加:事实证明,这两种使用模式可以在 Kernighan 和 Ritchie 的 C 书籍中找到,大约第 60 页(取决于版本)。另一件值得注意的事情是,这两个用例都只涉及前向 goto。事实证明,MISRA C 2012 版(与 2004 版不同)现在允许 goto,只要它们只是向前的。
在 Perl 中,使用标签从循环中“转到”——使用“last”语句,这与 break 类似。
这可以更好地控制嵌套循环。
传统的转到 标签 也受支持,但我不确定在很多情况下这是实现您想要的效果的唯一方法 - 子例程和循环应该足以满足大多数情况。
“goto”的问题和“无 goto 编程”运动最重要的论点是,如果你使用它太频繁,你的代码虽然可能表现正确,但会变得不可读、不可维护、不可审查等。在 99.99% 的情况下,“goto”会导致意大利面条式代码。就我个人而言,我想不出任何好的理由来解释为什么我会使用“goto”。
Edsger Dijkstra 是一位在该领域做出重大贡献的计算机科学家,也因批评 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;
}
当您需要更改释放语句中的某些内容(每个语句在代码中使用一次)时,第二个版本使事情变得更容易,并且在添加新分支时减少了跳过其中任何一个的机会。将它们移动到函数中不会有帮助,因为释放可以在不同的“级别”完成。
有人说 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;
将其与无 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;
我看到这些差异:
- 嵌套的
{}
需要块(尽管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 本身。请注意,标签在 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:;