将goto用于这些明显且相关的情况下,它有什么不好?


40

我一直都知道那goto是不好的事情,被锁在一个地下室,永远不会永远变好,但是今天我遇到了一个代码示例,使用起来很合理goto

我有一个IP,需要检查它是否在IP列表中,然后继续执行代码,否则引发异常。

<?php

$ip = '192.168.1.5';
$ips = [
    '192.168.1.3',
    '192.168.1.4',
    '192.168.1.5',
];

foreach ($ips as $i) {
    if ($ip === $i) {
        goto allowed;
    }
}

throw new Exception('Not allowed');

allowed:

...

如果我不使用,goto那么我必须使用一些变量,例如

$allowed = false;

foreach ($ips as $i) {
    if ($ip === $i) {
        $allowed = true;
        break;
    }
}

if (!$allowed) {
    throw new Exception('Not allowed');
}

我的问题是,goto当将其用于如此明显且与imo相关的情况时,有什么不好?


评论不作进一步讨论;此对话已转移至聊天
maple_shaft

Answers:


120

GOTO本身并不是一个直接的问题,它是人们倾向于使用它实现的隐式状态机。对于您的情况,您需要代码来检查IP地址是否在允许的地址列表中,因此

if (!contains($ips, $ip)) throw new Exception('Not allowed');

因此您的代码要检查条件。在这里,无关紧要的算法是无关紧要的,因为在主程序的思维空间中,检查是原子的。那是应该的。

但是,如果将执行检查的代码放入主程序,则会丢失该代码。您明确引入可变状态:

$list_contains_ip = undef;        # STATE: we don't know yet

foreach ($ips as $i) {
  if ($ip === $i) {
      $list_contains_ip = true;   # STATE: positive
      break;
  }
                                  # STATE: we still don't know yet, huh?                                                          
                                  # Well, then...
  $list_contains_ip = false;      # STATE: negative
}

if (!$list_contains_ip) {
  throw new Exception('Not allowed');
}

$list_contains_ip您唯一的状态变量在哪里,或隐式在哪里:

                             # STATE: unknown
foreach ($ips as $i) {       # What are we checking here anyway?
  if ($ip === $i) {
    goto allowed;            # STATE: positive
  }
                             # STATE: unknown
}
                             # guess this means STATE: negative
throw new Exception('Not allowed');

allowed:                     # Guess we jumped over the trap door

如您所见,GOTO构造中有一个未声明的状态变量。这本身不是问题,但是这些状态变量就像鹅卵石一样:搬运一个并不难,装满一个袋子会使您出汗。您的代码将保持不变:下个月将要求您区分私有地址和公共地址。之后的一个月,您的代码将需要支持IP范围。明年,有人会要求您支持IPv6地址。很快,您的代码将如下所示:

if ($ip =~ /:/) goto IP_V6;
if ($ip =~ /\//) goto IP_RANGE;
if ($ip =~ /^10\./) goto IP_IS_PRIVATE;

foreach ($ips as $i) { ... }

IP_IS_PRIVATE:
   foreach ($ip_priv as $i) { ... }

IP_V6:
   foreach ($ipv6 as $i) { ... }

IP_RANGE:
   # i don't even want to know how you'd implement that

ALLOWED:
   # Wait, is this code even correct?
   # There seems to be a bug in here.

谁必须调试该代码,谁就会诅咒您和您的孩子。

Dijkstra这样说:

立即使用go to语句会立即导致难以找到有意义的坐标集来描述过程进度的困难。

这就是为什么GOTO被认为是有害的。


22
$list_contains_ip = false;声明似乎放错了地方
Bergi

1
一个装满卵石的袋子很容易,我有一个方便的袋子来装它们。:p
whatsisname

13
@Bergi:你是对的。这表明弄清这些东西是多么容易。
wallenborn '16

6
@whatsisname那个包称为保存它们的函数/类/包。使用gotos就像杂耍它们然后将袋子扔掉。
法尔科

5
我喜欢Dijkstra的话,我没听过这么说,但这是一个很好的观点。突然之间,您有了可以随机跳转到整个页面的东西,而没有从上到下的流程。简而言之,它令人印象深刻。
比尔K

41

有一些合法的用例GOTO。例如,用于C中的错误处理和清除或用于实现某些形式的状态机。但这不是这些情况之一。第二个示例是更具可读性的恕我直言,但更具可读性的是将循环提取到单独的函数,然后在找到匹配项时返回。更好的是(用伪代码,我不知道确切的语法):

if (!in_array($ip, $ips)) throw new Exception('Not allowed');

那么,有什么不好GOTO的呢?结构化编程使用功能和控制结构来组织代码,因此语法结构反映了逻辑结构。如果仅条件执行某项,则它将出现在条件语句块中。如果某些内容在循环中执行,它将出现在循环块中。GOTO使您可以通过任意跳转来避开语法结构,从而使代码难以遵循。

当然,如果没有其他选择,可以使用GOTO,但是如果可以通过功能和控制结构实现相同的效果,则它是可取的。


5
@jameslarge it's easier to write good optimizing compilers for "structured" languages.关心详细吗?作为反例,Rust的人们介绍了MIR,它是编译器中的一种中间表示形式,它用gotos专门替换循环,继续,中断等,因为它更易于检查和优化。
8bittree '16

6
“如果你没有其他选择使用GOTO”这将是极其罕见的。老实说,我无法想到除了完全荒唐的限制(无法重构现有代码或类似的废话)之外,您别无选择。
jpmc26

8
此外,IMO goto使用带有大鼠嵌套的标志和条件嵌套(如OP的第二个示例)来模拟,比使用jut突出显示的可读性要差得多goto

3
@ 8bittree:结构化编程旨在用于源语言,而不是编译器的目标语言。本机编译器的目标语言是CPU指令,就编程语言而言,它绝对不是 “结构化”的。
罗伯特·哈维

4
@ 8bittree有关MIR的问题的答案在您提供的链接中:“但是在MIR中具有这样的构造很好,因为我们知道它只会以特定的方式使用,例如表示循环或中断。” 换句话说,因为Rust不允许goto,所以他们知道它是结构化的,并且具有goto的中间版本也是。最终,代码必须像机器命令一样深入到goto。我从您的链接中得到的是,他们只是在从高级语言到低级语言的转换中增加了一个增量步骤。
JimmyJames

17

就像其他人所说的那样,问题不在goto于其本身,而是它本身。问题在于人们的使用方式goto,以及如何使代码难以理解和维护。

假定以下代码段:

       i = 4;
label: printf( "%d\n", i );

打印什么值i什么时候打印?在不考虑函数中的每个实例之前goto label,您都不知道。该标签的简单存在会破坏您通过简单检查来调试代码的能力。对于具有一个或两个分支的小型功能,问题不大。对于不小的功能...

早在90年代初,我们就得到了一大堆C代码,它们驱动3D图形显示并被告知使其运行更快。它只有大约5000行代码,但所有代码在中main,并且作者goto在两个方向上使用了大约15个左右的分支。开头的代码很糟糕,但是这些代码的存在goto使它变得更糟。我的同事花了大约2周的时间才能弄清控制流程。更好的是,这些goto导致代码与自身紧密耦合,以至于我们在不破坏某些内容的情况下无法进行任何更改。

我们尝试使用1级优化进行编译,然后编译器先吞噬了所有可用的RAM,然后吞噬了所有可用的交换空间,然后恐慌了系统(这可能与gotos本身无关,但是我喜欢把那个轶事扔在那里)。

最后,我们为客户提供了两种选择-让我们从头开始重写整个内容,或者购买速度更快的硬件。

他们购买了更快的硬件。

博德的使用规则goto

  1. 仅向前分支;
  2. 不要绕过控制结构(即不要分支到ifor forwhilestatement 的主体中);
  3. 不要goto代替控制结构使用

在某些情况下,a goto 正确的答案,但很少见(突破深层嵌套的循环是我唯一使用它的地方)。

编辑

扩展最后一条语句,这是的几个有效用例之一goto。假设我们具有以下功能:

T ***myalloc( size_t N, size_t M, size_t P )
{
  size_t i, j, k;

  T ***arr = malloc( sizeof *arr * N );
  for ( i = 0; i < N; i ++ )
  {
    arr[i] = malloc( sizeof *arr[i] * M );
    for ( j = 0; j < M; j++ )
    {
      arr[i][j] = malloc( sizeof *arr[i][j] * P );
      for ( k = 0; k < P; k++ )
        arr[i][j][k] = initial_value();
    }
  }
  return arr;
}

现在,我们有一个问题-如果其中一个malloc呼叫中途失败怎么办?不太可能发生这样的事件,我们既不希望返回部分分配的数组,也不想因错误而退出函数。我们想自己清理一下并释放所有部分分配的内存。用一种在错误的分配上引发异常的语言,这非常简单-您只需编写一个异常处理程序以释放已分配的内容。

在C语言中,您没有结构化的异常处理。您必须检查每个malloc调用的返回值并采取适当的措施。

T ***myalloc( size_t N, size_t M, size_t P )
{
  size_t i, j, k;

  T ***arr = malloc( sizeof *arr * N );
  if ( arr )
  {
    for ( i = 0; i < N; i ++ )
    {
      if ( !(arr[i] = malloc( sizeof *arr[i] * M )) )
        goto cleanup_1;

      for ( j = 0; j < M; j++ )
      {
        if ( !(arr[i][j] = malloc( sizeof *arr[i][j] * P )) )
          goto cleanup_2;

        for ( k = 0; k < P; k++ )
          arr[i][j][k] = initial_value();
      }
    }
  }
  goto done;

  cleanup_2:
    // We failed while allocating arr[i][j]; clean up the previously allocated arr[i][j]
    while ( j-- )
      free( arr[i][j] );
    free( arr[i] );
    // fall through

  cleanup_1:
    // We failed while allocating arr[i]; free up all previously allocated arr[i][j]
    while ( i-- )
    {
      for ( j = 0; j < M; j++ )
        free( arr[i][j] );
      free( arr[i] );
    }

    free( arr );
    arr = NULL;

  done:
    return arr;
}

我们可以不用使用它goto吗?我们当然可以-它只需要做一些额外的簿记(实际上,这就是我要走的路)。但是,如果您正在寻找使用a goto并非立即表明不良做法或设计的迹象的地方,这就是少数几个。


我喜欢这些规则。我仍然不会使用goto,即使按照这些规则,我不会在使用它所有的,除非它是在分支可用的唯一形式语言,但我可以看到,这不会是一场噩梦调试的太多goto如果它们是根据这些规则编写的。
通配符

如果您需要摆脱一个深层嵌套的循环,只需使该循环成为另一个函数并返回
bunyaCloven16年

6
正如某人曾经为我总结的那样:“这不是一个必须解决的问题,它是一个来自问题。” 作为软件优化专家,我同意非结构化的控制流会使编译器(和人类!)陷入循环。我曾经花了几个小时在BLAS函数中破译一个简单的状态机(五个状态中的四个)GOTO,如果我没记错的话,它使用了多种形式的,包括Fortran的(in)著名的赋值和算术goto。
njuffa

@njuffa“作为软件优化专家,我同意非结构化的控制流可能会使编译器(和人类!)陷入循环。” -还有更多细节吗?我认为当今几乎所有优化的编译器都将SSA用作IR,这意味着它在下面使用了类似于goto的代码。
Maciej Piechotka '16

@MaciejPiechotka我不是编译器工程师。是的,现代编译器似乎都在其中间表示形式中使用SSA。编译器更喜欢单项单出口控制结构,不确定使用SSA会如何发挥作用?通过观察生成的机器代码和编译器资源的使用,以及与编译器工程师的讨论,非结构化的控制流可能会干扰优化代码转换,这大概是由于“来自”问题。如果编译器由于过于昂贵而决定跳过优化,则资源使用(时间,内存)可能会对代码性能产生负面影响。
njuffa

12

returnbreakcontinuethrow/ catch本质上都是goto的-他们都控制转移到另一段代码,并可以全部使用goto的实现-事实上,我在一所学校的项目这样做一次,PASCAL教练在说帕斯卡尔如何更好地比基本因为结构...所以我不得不相反...

关于软件工程,最重要的事情(我将在“编码”中使用该术语来指代有人与其他需要持续改进和维护的工程师一起创建代码库的情况,请您付钱)- -让它做某事几乎是次要的。您的代码只会被编写一次,但是在大多数情况下,人们将花费数天和数周的时间来重新研究/重新学习,改进和修复它-每次他们(或您)都必须从头开始并尝试记住/构想出来您的代码。

多年来,已添加到语言中的大多数功能是使软件更易于维护,而不是更容易编写(尽管有些语言朝着这个方向发展-它们经常会引起长期问题……)。

与类似的流控制语句相比,GOTO可以按照其最佳状态(在您所建议的情况下使用单个goto)进行跟踪操作几乎一样容易,并且在被滥用时是一场噩梦-而且很容易被滥用...

因此,在应对意大利面条噩梦几年后,我们只是说“不”,作为一个社区,我们将不会接受这一点-如果有一点余地,太多人会把它弄乱了-这确实是他们的唯一问题。您可以使用它们...但是,即使是最理想的情况,下一个人也会假设您是一个糟糕的程序员,因为您不了解社区的历史。

还开发了许多其他结构来使您的代码更易于理解:函数,对象,作用域,封装,注释(!)...以及更重要的模式/过程,例如“ DRY”(防止重复)和“ YAGNI” (减少代码的过度概括/复杂化)-所有这些实际上仅是让NEXT家伙读取您的代码(在您一开始忘记了大部分操作之后,可能就是您)!


3
我只是为了这样一句话:“最重要的是……使代码可读;使它执行某事几乎是次要的。” 我要说的只是略有不同。与其说使代码简单,不如说使代码具有可读性。但无论哪种方式,它是哦,所以,真的,使它做的事情都是次要的。
2016年

“ ,和/ 本质上都是goto的”不同意,前者尊重的原则结构化程序设计(虽然你可能会说有关的例外),去到绝对没有。鉴于这个问题,这一说法没有多大用处。returnbreakcontinuethrowcatch
埃里克

@eric您可以使用GOTO为任何语句建模。您可以确保GOTO都同意结构化的编程原则,它们只是具有不同的语法,并且必须完全包含相同的语法,才能完全复制功能,并包括“ if”之类的其他操作。结构化版本的优势在于它们仅限于支持某些目标-当您看到目标所在位置的连续内容时,您便会知道。我提到它的原因是要指出问题是问题的可滥用性,而不是不能正确使用。
比尔K

7

GOTO是一个工具。它可以用于善恶。

在糟糕的过去,使用FORTRAN和BASIC,它几乎是唯一的工具。

在看那些日子的代码时,当您看到一个代码时GOTO,必须弄清楚它为什么在那里。它可以是您可以快速理解的标准习语的一部分...,也可以是某些本不应该的噩梦般的控制结构的一部分。您只有看了才知道,很容易被误解。

人们想要更好的东西,并且发明了更高级的控制结构。这些涵盖了大多数用例,那些被坏消息烧死GOTO的人想完全禁止它们。

具有讽刺意味的是,GOTO在罕见的情况下还不错。当您看到一个标签时,就会知道有什么特别的事情发生,并且很容易找到相应的标签,因为它是附近的唯一标签。

快进到今天。您是一位讲授编程的讲师。您可能会说:“在大多数情况下,您应该使用高级的新结构,但在某些情况下,GOTO更容易阅读。” 学生不会理解这一点。他们将滥用GOTO代码以使其无法读取。

相反,你说:“ GOTO坏了。GOTO邪恶的。 GOTO失败的考试。” 学生将了解的是


4
实际上,对于所有已弃用的事物都是如此。全球 一字母变量名称。自修改代码。评估 甚至,我怀疑未经过滤的用户输入。一旦我们做好了避免诸如瘟疫之类的事情的准备,那么我们便处于合适的位置,以评估对它们的某种合理使用是否正确地解决了特定问题。在此之前,我们最好将它们视为被禁止的对象。
Dewi Morgan

4

除之外gotoPHP(和大多数语言)中的所有流构造都按层次进行作用域划分

想象一下通过斜眼检查的一些代码:

a;
foo {
    b;
}
c;

无论控制构造foo是(ifwhile等等),对于,和a,只有某些允许的顺序。bc

你可以有a- b- ca- c,甚至a- b- b- b- c。但是,你可能永远不会b- ca- - b- 。ac

...除非有goto

$a = 1;
first:
echo 'a';
if ($a === 1) {
    echo 'b';
    $a = 2;
    goto first;
}
echo 'c'; 

goto(尤其是向后goto)可能会很麻烦,以至于最好不要管它,而是使用分层的阻塞流构造。

goto有一个地方,但主要是作为底层语言的微优化。IMO,在PHP中没有合适的位置。


仅供参考,示例代码的编写甚至比您的任何建议都更好。

if(!in_array($ip, $ips, true)) {
    throw new Exception('Not allowed');
}

2

在低级语言中,GOTO是不可避免的。但是从高层次上讲,应避免使用(在语言支持的情况下),因为这会使程序更难以阅读

一切归结为使代码更难阅读。可以使用高级语言,比低级语言(例如汇编器或C语言)更易于阅读代码。

GOTO不会导致全球变暖,也不会导致第三世界的贫困。这只会使代码更难阅读。

大多数现代语言都具有使GOTO变得不必要的控制结构。有些像Java甚至没有它。

实际上,术语“ spaguetti代码”来自复杂的,难以理解的代码,是由非结构化分支结构引起的。


6
goto可以使代码易于阅读。当您创建额外的变量和嵌套分支而不是单跳时,代码很难阅读和理解。
马修·怀特

@MatthewWhited也许如果十行方法中只有一个goto。但是大多数方法都不长十行,大多数时候人们使用GOTO时就不会“仅一次”使用它。
图兰斯·科尔多瓦

2
@MatthewWhited实际上,术语“ spaguetti代码”来自复杂的,难以遵循的非结构化分支结构所导致的代码。还是spaguetti代码现在更容易变红了?也许。乙烯基回来了,为什么不去。
图兰斯·科尔多瓦

3
@Tulains我必须相信存在如此糟糕的事情,因为人们一直在抱怨,然后,但是我看到的大多数时候goto不是复杂的意大利面条式代码的示例,而是相当简单的事情,通常是在不使用语言的情况下模拟控制流的模式它没有语法:两个最常见的示例是打破多个循环,而OP中goto用于实现for- 的示例else

1
非结构化的意大利面条式代码来自不存在功能/方法的语言。 goto对于异常重试,回滚,状态机之类的事情也非常有用。
马修·怀特

1

goto语句本身没有错。一些不恰当地使用该语句的人有错。

除了JacquesB所说的(C语言中的错误处理)之外,您还goto用于退出非嵌套循环,您可以使用来做一些事情break。在这种情况下,您最好使用break

但是,如果您有嵌套循环的情况,则使用起来goto会更优雅/更简单。例如:

$allowed = false;

foreach ($ips as $i) {
    foreach ($ips2 as $i2) {
        if ($ip === $i && $ip === $i2) {
            $allowed = true;
            goto out;
        }
    }
}

out:

if (!$allowed) {
    throw new Exception('Not allowed');
}

上面的示例对您的特定问题没有意义,因为您不需要嵌套循环。但我希望您只看到其中的嵌套循环部分。

重点:如果您的IP列表很小,那么您的方法很好。但是如果列表增加,请知道您的方法具有O(n)的渐近最差运行时复杂度。随着列表的增加,您可能希望使用其他方法来实现O(log n)(例如树结构)或O(1)(无冲突的哈希表)。


6
我不确定PHP,但是许多语言都支持使用break和并continue带有数字(中断/继续循环很多层)或标签(中断/继续循环带有该标签),通常都应该使用最好goto用于嵌套循环。
8bittree '16

@ 8bittree好点。我不知道PHP的突破就是那么花哨。我只知道C的休息不是那么花哨。Perl的中断(他们称其为last)也曾经与C 的中断(即不像PHP的幻想)相似,但是在某些时候它也变得奇特。我猜我的答案越来越有用,因为越来越多的语言引入了break的花哨版本:)
Caveman

3
如果没有额外的变量,使用深度值中断将无法解决问题。自身的变量会增加复杂性。
马修·怀特

当然,仅仅因为您有嵌套循环并不意味着您需要goto,还有其他方法可以优雅地处理这种情况。
NPSF3000 '16

@ NPSF3000您能否提供一个嵌套循环的示例,您可以在不使用goto语句的情况下退出该嵌套循环,同时还使用一种不带有指定深度的break版本的语言?
穴居人

0

使用goto,我可以编写更快的代码!

真正。不在乎

后藤存在于装配中!他们只是称它为jmp。

真正。不在乎

Goto更简单地解决问题。

真正。不在乎

在经过严格训练的开发人员手中,可以轻松使用goto进行阅读。

真正。但是,我一直是那个训练有素的编码员。我已经看到随着时间的推移代码会发生什么。 Goto开始就好。然后重新使用代码的冲动就开始了。很快,我发现自己处在断点,甚至在查看程序状态后也没有该死的线索。 Goto使得很难推理代码。我们已经很努力创造whiledo whileforfor each switchsubroutinesfunctions,和更多的一切,因为这样做的东西与ifgoto难以对大脑。

所以不行。我们不想看goto。确保它在二进制文件中仍然有效,但是我们不需要在源代码中看到它。实际上,if开始看起来有些不稳定。


1
“ Goto更简单地解决了问题。” /“是的。不在乎。” -我不希望看到您的代码,在这里您故意避免使用简单的解决方案,而使用最可扩展的代码。(听说过YAGNI?)
user253751 '16

1
@Wildcard if和goto是解决问题的非常简单的方法,而其他方法则是最好的解决方法。它们是低挂的水果。与特定语言无关。Goto使您可以通过使其他问题成为代码问题来抽象出问题。 If让您选择该抽象。有更好的抽象方法和更好的选择抽象方法。 多态性为一。
candied_orange

2
尽管我同意你的看法,但这是一个措辞不佳的答案。“真的,不在乎”对于任何事情都不是令人信服的论点。我认为您的主要信息是:尽管goto吸引了喜欢使用高效,强大工具的人们,但即使采取了谨慎措施,这也会导致以后浪费大量时间。
Artelius

1
@artelius“真实,无关紧要”主题并不具有说服力。它旨在说明我将承认的很多观点,但仍然会反对它。因此,与我争论这些观点是没有意义的。既然那不是我的意思。明白这点?
candied_orange

1
我只是相信一个好的答案不仅可以陈述您的观点,而且可以解释它,并允许读者做出自己的判断。就目前而言,尚不清楚为什么 goto的优点不足以解决其问题。我们大多数人花费了数小时的时间来寻找涉及goto的错误。它不是唯一难以推理的结构。有人问这个问题可能经验不足,需要多加考虑。
Artelius

-1

汇编语言通常只有条件/无条件的跳转(相当于GOTO。较旧的FORTRAN和BASIC实现没有经过计数的迭代(DO循环)就没有控制块语句,而所有其他控制流都留给了IF和GOTO。这些语言以带有数字标记的语句终止,因此,为这些语言编写的代码可能(而且通常)很难遵循并且容易出错。

为了强调这一点,我们精心设计了“ COME FROM ”语句。

几乎不需要使用C,C ++,C#,PASCAL,Java等语言使用GOTO;可以使用替代结构,几乎可以肯定它是同样有效的,并且更易于维护。的确,源文件中的一个GOTO不会有问题。问题在于,不需要花很多时间就能使一个代码单元难以遵循且易于维护。这就是为什么人们公认的智慧就是尽可能避免使用GOTO。

关于goto语句的这篇维基百科文章可能会有所帮助

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.