长期以来,我一直认为goto
如果可能的话,永远不要使用它。
然而,前几天在仔细阅读 libavcodec(用 C 语言编写)时,我惊讶地发现它有多种用途。
goto
使用支持循环和函数的语言是否有利?如果是这样,为什么?请提供一个明确证明使用goto
.
长期以来,我一直认为goto
如果可能的话,永远不要使用它。
然而,前几天在仔细阅读 libavcodec(用 C 语言编写)时,我惊讶地发现它有多种用途。
goto
使用支持循环和函数的语言是否有利?如果是这样,为什么?请提供一个明确证明使用goto
.
每个反对者都goto
直接或间接地引用 Edsger Dijkstra 的GoTo Considered Harmful文章来证实他们的立场。太糟糕了,Dijkstra 的文章与如今使用语句的方式几乎没有任何goto
关系,因此文章所说的对现代编程场景几乎没有适用性。较少的goto
模因现在接近于一种宗教,一直到其从高处、其大祭司和被认为是异端的回避(或更糟)的经文。
让我们将 Dijkstra 的论文放在上下文中,以便对这个主题有所了解。
当 Dijkstra 写他的论文时,当时流行的语言是非结构化的程序语言,比如 BASIC、FORTRAN(早期的方言)和各种汇编语言。对于使用高级语言的人来说,以扭曲、扭曲的执行线程在他们的代码库中跳跃是很常见的,这导致了术语“意大利面条代码”的出现。你可以通过跳到由 Mike Mayfield 编写的经典 Trek 游戏并试图弄清楚事情是如何运作的来看到这一点。花点时间看一下。
这是 Dijkstra 在 1968 年在他的论文中抨击的“对 go to 语句的无节制使用”。 这是他生活的环境导致他写了那篇论文。在你喜欢的任何时候在你喜欢的任何地方跳转你喜欢的任何地方的能力是他批评并要求停止的。将其与goto
C 或其他此类更现代语言的贫乏能力进行比较简直是可笑的。
当他们面对异教徒时,我已经能听到邪教徒们高声吟唱。“但是,”他们会高呼,“你可以用 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 程序员,我会更仔细地研究大约 50% 的使用#define
long ,然后才开始反对goto
!
因此,对于那些费心读到这里的人来说,有几个关键点需要注意。
goto
语句的论文是为编程环境编写的,
与大多数不是汇编程序的现代语言相比,该环境goto
具有更大的潜在破坏性。goto
就像说“我曾经试图玩得开心但不喜欢它,所以现在我反对它”一样合理。goto
语句的合法用途无法被其他结构充分替代。godo
“可憎”,其中一个永远为假的do
循环被打破,而break
不是使用goto
. 这些通常比明智地使用goto
.使用我知道的“goto”语句有几个原因(有些人已经谈过了):
干净地退出函数
通常在一个函数中,你可能会分配资源并且需要在多个地方退出。程序员可以通过将资源清理代码放在函数末尾来简化他们的代码,并且函数的所有“退出点”都将转到清理标签。这样,您不必在函数的每个“退出点”都编写清理代码。
退出嵌套循环
如果您处于嵌套循环中并且需要跳出所有循环,那么 goto 可以使这比 break 语句和 if-checks 更清晰和简单。
低级性能改进
这仅在性能关键代码中有效,但 goto 语句执行得非常快,并且可以在通过函数时为您提供动力。然而,这是一把双刃剑,因为编译器通常无法优化包含 goto 的代码。
请注意,在所有这些示例中,goto 仅限于单个函数的范围。
盲目地遵循最佳实践并不是最佳实践。goto
避免将语句作为流控制的主要形式的想法是避免产生不可读的意大利面条代码。如果在正确的地方谨慎使用,它们有时可能是表达想法的最简单、最清晰的方式。Walter Bright,Zortech C++ 编译器和 D 编程语言的创造者,经常使用它们,但很明智。即使有这些goto
语句,他的代码仍然是完全可读的。
底线:goto
为避免而避免goto
是没有意义的。你真正想要避免的是产生不可读的代码。如果您的goto
-laden 代码是可读的,那么它没有任何问题。
好吧,总有一件事比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
由于goto
使关于程序流的推理变得困难1(又名“意大利面条代码”),goto
通常仅用于补偿缺失的功能:使用goto
实际上可能是可以接受的,但前提是该语言不提供更结构化的变体来获得同一个目标。以怀疑为例:
我们使用的 goto 规则是 goto 可以跳转到函数中的单个退出清理点。
这是真的——但前提是该语言不允许使用清理代码(例如 RAIIfinally
或采用结构化异常处理(但除非在非常低的级别,否则您永远不会遇到这种情况)。
在大多数其他语言中,唯一可接受的用途goto
是退出嵌套循环。即使在那里,将外部循环提升为自己的方法并使用它几乎总是更好return
。
除此之外,这goto
表明没有对特定代码进行足够的思考。
1goto
支持实现一些限制的现代语言(例如goto
可能不会跳入或跳出函数)但问题基本上保持不变。
顺便说一句,其他语言功能当然也是如此,尤其是例外。并且通常有严格的规则,只在指定的地方使用这些特性,例如不使用异常来控制非异常程序流的规则。
在C# switch语句中不允许直接通过. 所以goto用于将控制转移到特定的 switch-case 标签或默认标签。
例如:
switch(value)
{
case 0:
Console.WriteLine("In case 0");
goto case 1;
case 1:
Console.WriteLine("In case 1");
goto case 2;
case 2:
Console.WriteLine("In case 2");
goto default;
default:
Console.WriteLine("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 的设备等的评论。这都是关于使用,而不是滥用。
(相同的注释可以适用于longjmp
、异常、call/cc
等等——它们有合法用途,但很容易被滥用。例如,在完全非异常情况下,纯粹为了逃避深度嵌套的控制结构而抛出异常.)
这些年来,我写了几行以上的汇编语言。最终,每种高级语言都编译为 goto。好的,称它们为“分支”或“跳跃”或其他任何名称,但它们是 goto。任何人都可以编写 goto-less 汇编程序吗?
现在可以肯定,您可以向 Fortran、C 或 BASIC 程序员指出,使用 goto 进行 riot 是制作意大利肉酱面的秘诀。然而,答案不是避免它们,而是小心使用它们。
刀可用于准备食物、释放某人或杀死某人。我们是否因为害怕后者而没有刀具?同样的 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 有用的所有情况,dissing任何你能做到的好的情况没想到。
尽管使用 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;
}
这样做的好处是,您的代码在 end 之后可以访问执行清理所需的所有内容,并且您已经设法大大减少了更改点的数量。另一个好处是,您的功能已经从拥有多个退出点变为只有一个。你不可能在没有清理的情况下意外地从函数中返回。
此外,由于 goto 仅用于跳转到单个点,因此您并不是在创建大量来回跳转的意大利面条代码以尝试模拟函数调用。相反,goto 实际上有助于编写更结构化的代码。
总之,goto
应始终谨慎使用,并作为最后的手段——但它有时间和地点。问题不应该是“你必须使用它”,而是“使用它是不是最好的选择”。
我们使用的 goto 规则是 goto 可以跳转到函数中的单个退出清理点。在真正复杂的函数中,我们放宽了该规则以允许其他向前跳转。在这两种情况下,我们都避免了在错误代码检查中经常出现的深度嵌套的 if 语句,这有助于提高可读性和维护性。
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 语句一样容易被滥用的替代结构的最深思熟虑和彻底的讨论是 Donald Knuth 的文章“使用 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__;
)
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
...
}
1)我所知道的 goto 最常见的用法是在不提供它的语言中模拟异常处理,即在 C 中。(上面的 Null 给出的代码就是这样。)看看 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 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 构造不允许编译器无法轻易找出的目标,例如 goto 到计算地址 (**),编译器在使用 goto 的函数中找到无法访问的代码并不比使用 Dijkstra 更难- 批准的代码。
(**) 脚注:在 Basic 的某些版本中可以转到计算的行号,例如 GOTO 10*x,其中 x 是一个变量。相当令人困惑的是,在 Fortran 中,“计算的 goto”指的是一种等效于 C 中的 switch 语句的结构。标准 C 不允许在语言中使用计算的 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() 函数对来模拟非本地退出/异常,但与其他语言提供的相比,它们有一些严重的缺点。Wikipedia 文章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 label 或 continue 标签。根据http://www.oracle.com/technetwork/java/simple-142616.html,这实际上是 gotos 在 C 中最常见的用例(他们说 90%),但在我的主观经验中,系统代码倾向于更频繁地使用 goto 进行错误处理。也许在科学代码或操作系统提供异常处理(Windows)的情况下,多级退出是主要的用例。他们并没有真正提供有关调查背景的任何细节。
编辑添加:事实证明这两种使用模式可以在 Kernighan 和 Ritchie 的 C 书中找到,大约第 60 页(取决于版本)。另一件值得注意的事情是,这两个用例都只涉及前向 goto。事实证明,MISRA C 2012 版(与 2004 版不同)现在允许 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;
将其与 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 {...} 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:;
如果是这样,为什么?
C 没有多级/标记中断,并且并非所有控制流都可以使用 C 的迭代和决策原语轻松建模。gotos 在纠正这些缺陷方面大有帮助。
有时使用某种标志变量来实现一种伪多级中断会更清楚,但它并不总是优于 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。为什么?
原因是因为我可以从程序的另一点插入 aGOTO FINAL
而无需控制 A 的值。
这个例子很明显,但是随着程序变得越来越复杂,看到这些东西的难度也会增加。
相关资料可参见 Dijkstra 先生的著名文章《反对 GO TO 语句的案例》
我遇到过 agoto
是一个很好的解决方案的情况,但我在这里或任何地方都没有看到这个例子。
我有一个 switch 案例,其中有几个案例最终都需要调用相同的函数。我还有其他情况,最后都需要调用不同的函数。
这看起来有点像这样:
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 ;
}
我没有给每个案例一个函数调用,而是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 ;
}
案例 1 到 3 都必须调用doStuffFor123()
,类似的情况 4 到 6 必须调用doStuffFor456()
等等。
在我看来,如果你正确使用 goto,它们是非常好的。最后,任何代码都像人们编写的那样清晰。使用 goto 可以制作意大利面条代码,但这并不意味着 goto 是意大利面条代码的原因。那个原因就是我们;程序员。如果我愿意,我还可以创建带有函数的意大利面条代码。宏也是如此。
在 Perl 中,使用标签从循环中“转到” - 使用“last”语句,这类似于 break。
这允许更好地控制嵌套循环。
传统的 goto标签也受支持,但我不确定在太多情况下这是实现您想要的唯一方法 - 子例程和循环应该足以满足大多数情况。
'goto' 的问题和'goto-less programming' 运动的最重要论点是,如果你过于频繁地使用它,你的代码虽然可能表现正确,但变得不可读、不可维护、不可审查等。在 99.99% 的案例“goto”导致意大利面条代码。就个人而言,我想不出任何好的理由来解释我为什么要使用“goto”。
我在以下情况下使用 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;
}
当您需要更改释放语句中的某些内容(每个在代码中使用一次)时,第二个版本变得更容易,并且在添加新分支时减少了跳过其中任何一个的机会。在函数中移动它们在这里没有帮助,因为释放可以在不同的“级别”完成。
它不时用于按字符进行字符串处理。
想象一下像这个 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
如果您的逻辑使 else 案例不起作用,您仍然必须在调用之后使用语句(可以说是 goto 的一种形式)以获得相同的语义。
我还在词法分析器中明智地使用了 goto,通常用于类似的情况。大多数时候你不需要它们,但对于那些奇怪的情况来说,它们很高兴。
在该领域做出重大贡献的计算机科学家 Edsger Dijkstra 也因批评 GoTo 的使用而闻名。维基百科上有一篇关于他的论点的短文。