重新审视
《GOTO 语句被认为有害》


David R. Tribble
修订版本 1.1, 2005-11-27


Emon Wang
翻译版本 1.02,2021-12-29


  • 声明
  • 介绍
  • 背景
  • 第一部分 - Dijkstra 的信与批注
  • 第二部分 - 结构化程序设计
  • 第三部分 - Goto 还有使用必要性吗?
  • 总结 - Goto 之道
  • 参考文献

  • 声明

    原文 除引用信件部分外由 David R. Tribble 撰写,引用的信件由 Edsger W. Dijkstra 撰写,全文由 王一蒙(Emon Wang) 翻译为中文。版权声明 见文章底部。

    译者水平有限,难免有疏漏不足,敬请批评指正。如果您发现任何问题或者有改进意见,请在项目的 issue 页面 提一个issue。非常期待您的宝贵意见。


    介绍

    本文讨论和分析 Edsger W. Dijkstra 于1968年致计算机科学协会通讯 (CACM) 的信件。在信中,Dijkstra 呼吁废除编程语言中的 goto 语句。

    今天,这封信已经变得名闻遐迩(或声名狼藉,这取决于您对 goto 语句的感受)。这封信可能是关于程序设计被引用次数最多的文档,也可能是程序设计史上被阅读最少的文档。

    大多数程序员都听过“永不使用 goto 语句”的箴言,但如今只有少数计算机科学专业学生了解 Dijkstra 反对 goto 的历史背景并从中受益。“goto 语句是邪恶的”这句“传言”进入了现代编程的教条,但是阅读 Dijkstra 原文可以让你明白这句传言完全不得要领。

    Dijkstra 写那封信时,公认的方式是用 goto 语句手动编写循环if-then和其他控制结构,因为我们今天习以为常的基本控制流语句那时候要么不被大多数编程语言支持,要么形式有限制。Dijkstra 的意思不是 goto 的所有用途都不好,而是说正确使用高级的控制结构能消灭 大多数 当时流行的 goto 的用法。Dijkstra 仍然允许将 goto 用于更复杂的编程控制结构。


    背景

    Dijkstra 和其他人(C. A. R. Hoare,Niklaus Wirth等人)在早期为新兴的编程这门学科提供的工作的重要性不可低估。他们的贡献对于建立计算机科学这门严谨的学科以及将算法编程作为数学和逻辑学的官方分支极有意义。

    Dijkstra 就像他的大多数同事一样,是一位接受过大量数学培训的学者。计算机科学形成初期,毫不意外,许多工作都是为了让计算机编程成为一门具有扎实数学与逻辑基础的、严谨的工程学科。Dijkstra 和他的同事们的愿景是开发出一门编程语言,用这门编程语言写出的程序的正确性可以被证明。这种称为 形式验证 的理论是能设计出一小套编程结构(如if-then-else循环语句原始数据类型等),并且这套编程结构强大到能定义任何可能的编程任务,最后这些编程任务可以在数学上证明是正确的(即没有逻辑错误)。

    形式验证运动始于1950年代后期,其精神类似于之前希尔伯特(David Hilbert)提出的被称为希尔伯特纲领 (Hilbert's programme)的数学运动。这个数学运动旨在将所有数学用自然数的组成部分以及简单的逻辑和算术规则编纂为一套完整的,包罗万象的法则。遗憾的是,哥德尔不完备定理使这个梦想破灭。哥德尔不完备定理证明了有存在于逻辑可证明性领域之外的数学真理(和非真理)。

    形式验证运动也类似牛顿物理学:在牛顿运动定律被接受的初期,许多人投身于一个信念:物理宇宙是确定性的,只要事先足够了解其中涉及的质量和动量,所有运动和活动都可以被以任意精度计算。遗憾的是,海森堡,玻尔等人的发现带来了量子力学,从而结束了这一信念。他们意识到,在粒子层面上,所有物理行为本质上都是概率性和随机性的,是不可预测的。

    同样,形式验证的目标最终被认为是行不通的。Dijkstra 后来放弃探索程序可证明性而转向了研究准确的程序生成 (program derivation) 的技术。此技术让程序员能够用方法论有条理构造程序,确保程序表现出正确的行为。该研究领域与自顶向下设计(top-down design)和功能分解(functional decomposition)的技术有很多共同点。

    另外,今天被认为是理所当然的许多编程术语在1968年还没有被牢固地确立。当时对编程术语有很多争论和讨论。我们今天使用的很多术语花了很多年才被广泛接受。

    Dijkstra 在著作中倾向于使用严格的,冗长的措辞。这在一定程度上可以解释为什么很少有人读过他著名的“GOTO”信。值得注意的是,他鄙视用 bug 一词来表示编程错误,而宁愿用 error 一词。当然,今天这两个术语仍然在编程语境下使用,error 是指任何一个人或一台机器产生的错误(因为总有错误), bug 仅用于人造系统领域,特别是可编程计算机,表示设计中的特定故障或意外的执行结果。术语 debug 也应运而生,以表示在系统中查找和删除 bug 的特定活动。如果只留下 error 一词, debug 这个词也不会出现。


    第一部分

    Dijkstra 的信,带评注

    接下来是 Dijkstra 在1968年写给 CACM 的著名的“GOTO”信,以及从历史角度讨论这封信细节的评注。


    Go To 语句被认为有害
    Edsger W. Dijkstra

    Reprinted from Communications of the ACM,
    Vol. 11, No. 3, March 1968, pp. 147-148.
    Copyright ©1968, Association for Computing Machinery, Inc.

     
     

    David R. Tribble
    评注

    编辑:

    数年来我观察到,程序员的素质是一个递减函数,其中自变量是他们程序中 go to 语句的密度。最近我发现了为什么使用 go to 语句会造成灾难性的后果,因此我深信应该从所有“高级”编程语言(即除纯机器码的所有语言)中废除 go to 语句。当时我并不十分重视这个发现。现在,最近讨论中出现了这个话题,这敦促了我发表我的想法。

     
     

    Dijkstra 介绍他发现 goto 语句对它们所在的程序非常不利。他认为程序员使用的 goto 越多,程序员的能力就越差。

    他建议应从所有高级编程语言中废除 goto 语句。他甚至暗示应该从所有编程语言(可能包括机器代码)中删除 goto ,人们不禁要问这如何实现。

    我的第一句话是,尽管程序员的活动(activity) 在他完成了一个正确程序就结束了, 然而程序员的活动真正的主体部分,其实是这个程序的流程(process),因为这个流程必须要达到程序员所期望的效果,也必须在动态的表现上满足编程者所期望的规范。一旦程序被编写完成,相关流程的完成就交给机器了。

     
     

    这一段技术密集的措辞是 Dijkstra 学术写作风格的典型代表。

    这句话仅意味着程序员执行的实际活动不仅是编写程序,还包括控制在代码在机器执行时代码的行为。然而,一旦程序员编写完程序,该程序的实际执行就只靠机器来控制。

    Dijkstra 使用“正确”一词来描述没有 错误 (error) ,或者按照目前的说法,没有 bug 的程序。“错误”一词反映了当时可以写出能进行“形式化验证”的代码的信念,即代码可以进行一系列的数学和逻辑操作,证明该代码包含错误(逻辑错误,约束错误 (constraint error),不变错误 (invariant error) ...),或代码没有错误即可以证明是正确的。正如上面的背景部分所述,计算机科学家(或至少程序员)今天并不以这种方式考虑编程。

    我的第二句话是,我们的智力很适合掌握静态关系,而我们想象随时间变化的动态流程的能力相对较弱。出于这个原因,我们(既然明智的程序员意识到了我们的局限性)应该尽力缩短静态程序和动态流程之间的概念鸿沟,以尽可能让程序(在文本空间中扩展)和流程(在时间中扩展)之间的对应关联清晰。

     
     

    Dijkstra 在这里观察到,与动态关系相比,人类更擅长想象静态关系。因此他认为,将两种关系用程序代码来表达时,我们应将两种关系之间的差异最小化,以便在源代码本身的结构中明显看出程序的动态(非恒定)方面。

    对当前大多数以线性,按语句顺序的方式运行的编程语言来说这说法都是对的。但是,当我们观察到现实世界中的编程任务必须处理的复杂性(举几个例子,如多任务,多线程,中断处理,易失性硬件寄存器,虚拟内存分页,设备延迟,实时事件处理等等)时,在某种程度上,Dijkstra 的行为准则尚未完全实现。简单的一次执行一行的程序执行模型已不足以解决当今的编程问题。

    现在让我们考虑如何表示一个流程的进度 (progress of a process)。(您可能会以一种非常具体的方式考虑这个问题:假设一个流程(按时间顺序的一串行为(action) )在某个行为之后停止,我们必须确定哪些数据才能重新到流程停止的那一点?) 假设程序文本纯粹是赋值语句的序列(为方便讨论,赋值语句被视为单个行为的描述),则表示进度只要指向程序文本中两个 连续的行为的描述 (successive action description) 之间的一点就够了。

     
     

    Dijkstra 开始构建 程序执行 (program execution) 的正式定义,或他所谓的 流程进度(progress of a process) 。接下来的讨论类似于在 C 和 C++(和其他)语言所采用的执行模型的正式定义中使用的 序列点 (sequence point) 的定义。

    必须记住,我们今天想当然的许多术语在当时还没有牢固地确立,并且没有普遍接受的语言或伪语言用于论述算法和程序。当然,今天的作者会使用诸如 C,Java,Pascal,LISP 之类的具体语言,或者与这些语言中的一种非常相似的伪语言作为 通用语言 来说明编程概念。

    (在没有 go to 语句的情况下,我可以允许自己在上一句的后三个词(连续的行为的描述)中出现句法歧义:如果我们将它们解释成为“连续的(行为的描述)”,则表示在文本空间中是连续的;如果我们将它们解释为“(连续的行为)的描述”则是指时间上连续的。)让我们将指向文本中适当位置的指针称为“文本索引 (textual index)”。

     
     

    Dijkstra 在典型的学术风格中运用了一些语言上的小聪明,使词组 连续的行为的描述 具有两种不同的含义。这反映了他前面提到的编程任务的双重性质:这些任务与依次执行一个语句(或行为)的顺序性质有关,即程序的源代码由单独的语句(行为)组成序列以反映语句执行的时间顺序。

    他的术语 文本索引 (textual index) 实质上是一个 程序计数器 (program counter)。但是,他试图超越简单地跟踪当前线程 (thread) 执行到的位置,而在源代码文本中的语句与程序执行状态之间建立显式连接。因此,更好的术语可能是 语句指针 (statement pointer) 。

    当我们纳入 条件语句 (conditional clause) (if B then A), 可选择的条件语句 (alternative clauses) (if B then A1 else A2), 由C. A. R. Hoare引入的 选择语句 (choice clause) (case[i] of (A1, A2, ···, An)),或J. McCarthy引入的 条件表达式 (conditional expression) (B1 → E1, B2 → E2, ···, Bn → En), 事实上,流程进度还是由一个 文本索引 表示。

     
     

    Dijkstra 引入了更复杂的流控制语句 (flow control statement),例如 if-then-else条件语句和case (又称为selectswitch)选择语句,并指出这些不会改变其 文本索引 (或 语句指针 )的基本性质。

    这反映了这样一个事实,即当时人们在为编程语言和一般编程理论制定最佳的最小流控制结构集方面(minimal set of flow control structures)付出了很多努力。绝非偶然的是,大多数结构类似于 ALGOL 支持的控制结构,因为许多从事或影响 ALGOL 设计的人都是写了大量关于编程的东西的学者。

    这一切工作的主要目标是创建一种命名方法(nomeclature),这种命名方法不仅可以用于实际的编程语言,而且还可以直接用于编程算法的数学公式化。如上面的背景部分所述,这反映了一种信念,即可以用某种形式表达程序,从而可以用数学方式证明其正确性。

    一旦我们的语言里包括了 子过程 (procedure),我们就必须承认单一的文本索引已不足以描述流程进度了。在文本索引指向子过程体内部的情况下,只有当我们还同时给出是在哪一处调用子过程时才能表示动态流程的进度。包含了子过程,我们可以通过一系列文本索引来表征流程的进度,该序列的长度等于子过程调用的动态深度。

     
     

    这表明,如果程序使用 子过程 (通常称为过程,函数或方法), 则单个语句指针不足以定义正在执行的程序的状态 。为了处理这种额外的复杂性,Dijkstra 定义了 一系列文本索引 (a sequence of textual indices)。

    现代术语称此为 调用栈 (call stack),它是程序计数器(也称为 返回地址 )的数组,每个计数器都指明从哪条语句进行子过程调用。但由于 Dijkstra 正在建立文本索引与程序执行状态之间的关联,因此将调用堆栈视为语句指针数组会更正确。所需的语句指针的数量仅仅是在程序执行中给定的某一时刻起作用的子过程调用数量,即调用栈的深度。

    现在让我们考虑 重复语句 (repetition clause)(例如, while B repeat Arepeat A until B)。从逻辑上讲,这样的语句是多余的,因为我们可以借助递归过程来表示重复。出于现实的原因,我不希望将它们排除在外:一方面,重复语句在当今的设备已经相当好地实现了;另一方面,称为“归纳法”的推理模式使我们有足够的智力掌握重复语句产生的过程。

     
     

    Dijkstra 添加了新的控制流语句——重复语句。他顺便指出,这样的语句完全没有必要,因为可以用等效的递归调用来代替它们。

    这反映了一个事实,当时递归非常流行,许多人(尤其是学术界人士)认为递归是表示程序和算法的一种高级形式。流行的原因是,递归定义具有严格的数学历史,特别是 递归公式 (recursive formula) 和 递推关系 (recurrence relation) 。 递归公式 和 递推关系 处理递归定义的序列,其中序列中的每个元素都用简单的术语和该序列中的先前元素进行定义。两个经典示例是阶乘函数 n! = n(n-1)!, 以及斐波那契数列 Fi = Fi-2 + Fi-1.

    这是典型的学术观察。从理论上讲,可以将任何循环语句替换为递归调用,并且某些语言(例如LISP)实际上支持递归编程风格(也称为 函数编程 )。但是,对于大多数编程应用程序以及大多数编程语言实际支持的应用程序而言,递归仅扮演次要的角色(但仍然非常有用)。

    Dijkstra 提到,迭代语句可以在 资源有限的设备 (finite equipment)上实现 ,这当然是所有实际存在的计算机的本质,无论它们拥有多少虚拟内存。这种微妙的方式承认某些形式的递归需要潜在的无限资源(即无限调用堆栈)。考虑一个典型的嵌入式应用程序,它具有一个主程序循环,该循环轮询一个事件,处理该事件,然后等待下一个事件,永远这么循环。这样的无限循环确实可以写成尾递归过程调用,但是那有什么意义呢?使用这种更复杂的、为了递归而递归的方式将无法在大多数实际的系统上进行编程。

    Dijkstra 似乎暗示着迭代循环(归纳)语句在理论上比递归更难掌握,这是只有数学家会说的那种东西。

    迭代, 动词. - 参见 迭代.

    加上重复语句,文本索引已不足以描述流程的进度。然而,在每次进入重复语句时,我们都可以关联一个所谓的“动态索引”(dynamic index),从而不停地记录重复语句的进入次数。由于重复语句(就像过程调用一样)可以嵌套应用,因此我们发现,流程的进度现在可以通过文本索引和/或动态索引的(混合)序列来唯一地表示。

     
     

    通过添加重复控制结构,我们不仅需要一种指明当前语句的方法,而且还需要一种方法跟踪当前正在执行每个循环的哪次迭代 。因此,就像嵌套过程调用一样,我们必须使用 循环迭代堆栈 (loop iteration stack) 来跟踪这些迭代计数,每个(嵌套的)循环有一个这样的堆栈,Dijkstra称之为 动态索引序列 (dynamic index sequence)。

    因此,将它们组合到一起,我们有一个 文本索引序列( 调用堆栈 )和一个 动态索引序列 (循环迭代堆栈),它们共同定义了正在执行的程序的当前状态。

    (嵌套?)

    要点是这些索引的值都不在程序员的控制范围之内。无论程序员想不想,这些索引的值都是通过程序的编写或流程的动态演变生成的。这些索引提供了独立的坐标来描述流程的进度。

     
     

    Dijkstra 再次指出,显而易见的是,一旦编写并运行了程序,程序员将不再对实际程序执行具有任何控制权。程序的执行情况由执行过程中任何给定时间点的调用堆栈和循环迭代堆栈的内容表示—— Dijkstra 称为程序执行的 独立坐标 (independent coordinate),而我们可以将其简称为程序的 状态 (state)或 执行历史 (execution history)。

    为什么我们需要这样的独立坐标?原因是——这似乎是顺序流程所固有的——我们只能根据流程的进度来解释变量的值。如果我们希望计算一个最初是空的房间其中的人数 n 那么只要看到有人进入房间,我们就可以通过将 n 增加一来计数。在中间时刻,我们观察到有人进入房间但尚未执行随后的 n 的增加 , 此时n 的值等于房间中的人数减去一

     
     

    Dijkstra 指出,只有在给定的时间点之前准确地知道了程序的执行历史,才能知道程序中给定变量的值。换句话说,程序以 确定性的(deterministic) 方式执行,并且应该有可能通过执行历史(或程序状态的历史),确定任何变量到执行期间的任何时刻的值。

    Dijkstra 引入了在程序语句完成之前的 中间时刻 (in-between moment) 的概念。这类似于在 C 和 C++ 等语言中指定的 序列点 的概念,序列点精确定义了何时发生行为以及以什么顺序发生,同样重要的是,哪些行为没被定义。

    程序的执行仅在特定的序列点(通常发生在语句的末尾,函数调用之前)和子表达式求值的特定点上被良好地定义。在任何两个序列点之间,程序的状态都没有明确定义,这意味着,到达下一个序列点之前,程序变量的值处于不确定(或中间)状态。

    Dijkstra 给出了一个简单的例子:计数器的自增,或例如 n = n+1 的语句。当它们实际执行时,在某个时刻会已经读取 n 的先前值并将其添加一,但是尚未将新值写回到变量 n。这是他所指的 中间 状态,或者是两个 序列点 之间的执行状态 , 在此期间变量 n 仍包含其旧值而不是其新值。

    go to 语句滥用的直接后果是很难找到有意义的 坐标集 (set of coordinates) 来描述流程进度。通常,人们会把一些精心选择的变量的值纳入考虑,但这个方法办不到,因为要知道,根据流程的进度才能理解这些变量的值!当然,使用 go to 语句,仍然可以通过计数器来唯一地描述进度,该计数器(即一种标准化的时钟 (normalized clock))对自程序启动以来 执行的行为个数 (number of actions performed) 进行计数。困难在于,尽管这样的一个坐标是唯一的,但却完全无济于事。因为在这样 n 居然等于房间人数减一的坐标系统中,定义所有的流程点变得极度复杂!

     
     

    终于在这里,我们达到了 Dijkstra 关于卑微的 goto 的论点的症结。本质上,Dijkstra 认为,在程序中对 goto 语句的“无限制使用”会模糊程序的执行状态和历史记录,因此在任何给定时刻,调用堆栈和循环迭代堆栈的值不再足以确定程序变量的值。

    以下事实导致了这种混乱:不受约束的 goto 语句可以在控制完成之前将控制权从循环中移出,并且同样可以将控制权转移到已经迭代的循环中间。两种情况都使修改循环迭代堆栈中的计数器的方式变得复杂。

    除此之外,还有存在 非局部 (non-local) goto 的可能性, 即将控制权从当前正在执行的子过程转移回先前调用的子过程的goto的可能性,这实际上通过使整个调用堆栈的值无效来破坏执行状态。

    Dijkstra陈述了在 中间时刻 将控制从循环或过程中转移出的特定示例,这使执行状态从该点开始一直不确定。

    换种说法,goto 可以使应该由程序结构保证不侵犯的 程序不变量 (program invariant) 无效 。他在此处使用的示例不变量是,计数器 n 始终表示房间中的人数。允许非结构化的goto更改执行过程可能导致不变量变为无效(即不再是不变量了),从而使 n 的值变得毫无意义,或者至少从执行历史中确定 n 的真实值极为困难。

    现在的 go to 语句太原始了;太多地把程序弄得一团糟。如果控制 go to 的使用,它还是可以被考虑使用并被欣赏的。我并不是说我详尽无遗的提及了可满足所有需要的语句,但是无论提出什么语句(例如 终止语句 (abortion clauses)),它们都用有帮助的且易于管理的方式,满足了维护一个描述流程的,与程序员无关的坐标系 (a programmer independent coordinate system) 的要求。

     
     

    Dijkstra 说的 goto 语句所代表的其实是 非结构化 (unstructured) goto,也就是在其他结构化语言中没有任何限制的使用的 goto 语句。

    把 goto 语句的使用限制在一些简单的、结构良好的控制,例如从循环中提前退出,错误处理 (error handling)(又称异常 (exception))等等可以将 goto 语句带回到 结构化的控制流修正领域 (structured control flow modification)。但是,如果某种语言没有强制实施这些限制的规则,就不能说这语言提供的 goto 语句结构合理。

    Dijkstra 承认并非某种语言提供的所有流控制结构都可以满足所有编程需求。这意味着对于那些需要更复杂的流控制的罕见编程情况,goto 仍然占有一席之地。

    Dijkstra 提到了 终止语句 (abortion clause) 或现在通常称为 异常处理程序 (exception handlers)的内容,暗示这些东西实际上是可以通过定义使其在结构化语言的范围内表现良好的花哨 goto ,即这些 goto 不会以偶然的方式破坏执行状态

    像 Ada,C ++,Java 和其他面向对象语言的异常处理语句在大多数情况下都遵循该原则,因此当这些语言引发异常时,执行状态(包括全局变量和局部变量,过程调用堆栈,堆等)以干净且可预测的方式更改。

    更多原始语言(例如 FORTRAN,COBOL,C,Pascal 等)可以提供某些原始异常处理机制,但是使用它们不能保证干净地保留执行状态或正确释放分配的资源。

    用公正的承认来结束这篇文章是很难的。我要判断我的思想受到谁影响吗?显然我受到 Peter Landin 和 Christopher Strachey 的影响。最后,我想记录一下(我记得很清楚) Heinz Zemanek 在1959年初在哥本哈根举行的 pre-ALGOL 会议上如何明确表达了他的疑问,即对 go to 语句是否句法上应与赋值语句一视同仁。在一定程度上,我责怪自己没有得出他讲话的后果。

     
     

    Dijkstra在此说明在影响了他的人。参见背景,设计 ALGOL 语言的许多人也讨论了正确的语言设计和正确的程序控制流结构。

    go to 语句不合需要的说法并非新鲜事物。我记得读过明确的建议,建议上明确推荐只将 go to 用在警报时的退出 (alarm exit),但是我无法查到在哪。据推测,它是由C. A. R. Hoare制造的。在[1, Sec. 3.2.1] Wirth and Hoare 共同朝着推动 case 语句 的方向发表了看法:

    “与条件语句类似,它比 go to 语句和 switch 更清晰地反映了程序的动态结构,并且消除了在程序中引入大量 标签 (labels) 的需要。”
     
     

    Dijkstra 指出,可以将 goto 语句(仅)用于在警报时的退出——我们将其称为 致命异常(fatal exception)。对于缺乏健壮的异常处理机制的语言,goto 可能是唯一的实用替代品。

    Dijkstra 提到了 Hoare 和 Wirth 提出的 case (或 select)控制流结构的设计。今天,我们认为这种控制结构是理所当然的,但是当时仍在讨论它的优点。Dijkstra 提醒我们,它最初是 if , gotolabel 的笨拙替代方案。

    在 [2] 中,Guiseppe Jacopini 似乎证明了 go to 语句的(逻辑)多余。但是,不建议您尝试将某个流程图机械地转换为 无跳跃 (jump-less) 的流程图。不要期望这样得到的流程图比原始流程图更透明。

     
     

    Dijkstra 提到了流程图 (flow diagrams),这反映了当时程序设计的最新水平。从那时起,编程技术经历了结构化编程 (structured programming),自顶向下编程 (top-down programming),面向对象编程 (object-oriented programming),组件编程 (component programming),切面编程 (aspect programming) 等阶段的发展。然而,尽管已有程序语言设计上的这些进步,但 Dijkstra 关于 非结构化程序流 的主要观点历久弥新。

    必须指出的是,Dijkstra对这个问题的最终评论似乎暗示着,从一个人编写的程序中完全删除所有 goto 是一个坏主意。虽然 Dijkstra 说已证明对任何给定的程序 goto 语句其实都是多余的,他仍然承认,消除所有 在程序中的 goto 语句会使程序的控制流更加难以理解。

    他实际上是在论证程序中的某些 goto 可能有用,并且实际上可能会使程序更易于理解。因此,可以肯定地说Dijkstra认为goto语句是有害的 ,但不是致命的,并且肯定不是无用的

    References:

    1. Wirth, Niklaus, and Hoare C. A. R.
      A contribution to the development of ALGOL.
      Comm. ACM 9 (June 1966), 413-432.

    2. Böhm, Corrado, and Jacopini Guiseppe.
      Flow diagrams, Turing machines and languages with only two formation rules.
      Comm. ACM 9 (May 1966), 366-371.

    Edsger W. Dijkstra
    Technological University
    Eindhoven, The Netherlands

     


    第二部分

    结构化程序设计

    自从 Dijkstra 的那封信发表以来,程序语言是否已经发展到了 goto 语句不再被需要的地步?

    20世纪60年代以来,编程语言的理论已经出现了几次进步。编程语言的范式也已经发生了不少更替,每一个进步都标榜自己是未来的“更好的”程序设计方法。下面是这些进步的一个简表:

    每一个方法都导致了编程语言理论的范式更替、影响了程序员日常编写程序的方式并改变了编程语言的设计方法及其提供的功能。但所有这些进步对程序结构的影响都高于简单的执行语句的层次(即在 子过程 (procedure),数据对象 (data objects),程序模块 (program modules) 等的层次上)。在最底层的 顺序语句的层次 (sequential statement level)进行编程的基本方法与最初的编程语言(如 FORTRAN 和 COBOL)相同。

    以下各节描述了当今大多数编程语言中普遍可用的程序流结构。自从结构化编程问世以来,它们几乎保持不变。(这些构造以伪代码而不是任何特定语言显示。)


    if-then-else

    所有结构化编程语言提供某种形式的 if-then 流控制结构:

    if 条件表达式 then
        语句1
    

    以及 if-then-else 结构:

    if 条件表达式 then
        语句1
    else
        语句2
    

    第二个结构最明显的替代了 非结构化的 测试转向 (test-and-goto) 结构:

    if 条件表达式 then
        语句1
        goto endif1
    else1:
        语句2
    endif1:
        ...
    

    if-then 语句 可以被下面的机器码实现:

    # if-then statement
        move expression, reg1
        jump not condition, label1
        语句1
    label1:
        ...
    

    if-then-else 语句的机器码实现类似:

    # if-then-else 语句
        move expression, reg1
        jump not condition, label1
        语句1
        jump label2
    label1:
        语句2
    label2:
        ...
    

    常见的一种编程习惯是把多个 if-then 语句写成一个序列:

    if 条件1 then
        语句1
    else if 条件2 then
        语句2
    else if 条件3 then
        语句3
    else
        语句4
    

    在某些语言 (尤其是某些较老的语言) 里连写多个 if-then 很难,所以结构最后看上去像下面的代码,该代码功能等效但更难阅读:

    if 条件1 then
        语句1
    else
        if 条件2 then
            语句2
        else
            if 条件3 then
                语句3
            else
                语句4
            end
        end
    end
    

    某些语言给 else-if 这个组合提供了单独的关键字 (如 elseif, elsifelif)但它们的效果是一样的。 某些语言还给 if-then 序列最后的 else 语句提供了单独的关键字 (如 otherwisedefault).


    选择语句

    大多数结构化的编程语言都提供某种类型的 多个选择 的控制结构 (如 case, select, switch, examine, inspect, choose, when, 诸如此类). 设计出这个语句旨在替换 多个if-then的序列,让原本的意图更清楚,即,用给定的表达式值来选择多个选项之一:

    select 表达式 in
        case 常量1:
            语句1
    
        case 常量2:
            语句2
    
        case 常量3:
            语句3
    
        default:
            语句4
    end
    

    这与之前展示的的多个 if-then 语句序列是等价的,其中 default 充当最后的 else 语句. 某些语言不提供 defaultotherwise 语句。

    select 语句可以被下面的机器码实现:

    # select statement
        move expression, reg1
        cmp  reg1, constant1
        jump not equal, label1
        语句1
        jump label4
    label1:
        cmp  reg1, constant2
        jump not equal, label2
        语句2
        jump label4
    label2:
        cmp  reg1, constant3
        jump not equal, label3
        语句3
        jump label4
    label3:
        语句4
    label4:
        ...
    

    可能有效率更高的实现,例如 在跳转表中使用索引 (using an index into a jump table)或者把这些比较的次序比较重新排列以模拟展开后的二分搜索。 有些 CPU 提供了特殊的指令将 select 语句在机器码上实现成小的跳转表

    某些语言 (特别是 C, C++, Java, C#, 还有其他语法从 C 派生的语言) 通过允许每个 case 语句“落入”下一个 case 语句,从而允许更为复杂的控制流。纯粹主义者说,这会破坏逻辑上原本干净的控制结构,而实用主义者则说,它让许多困难的编程情况下代码更高效。


    do-while

    这是大部分结构化编程语言提供的最简单的迭代形式。通常提供两种变体,一种在循环体之前进行条件测试(至少迭代一次):

    do
        语句
    while 条件表达式
    

    另一种形式是将条件测试放在循环体之后(零次或多次迭代):

    while 条件表达式 do
        语句
    end
    

    这些结构在功能上等效于使用goto的以下代码:

    # do-while
    loop:
        语句
        if 条件表达式
            goto loop
    

    和:

    # while-do
    loop:
        if not 条件表达式
            goto endloop
        语句
        goto loop
    endloop:
        ...
    

    这些结构可以被下面的机器码实现:

    # do-while statement
    label1:
        语句
        move expression, reg1
        jump condition, label1
        ...
    

    # while-do statement
    label1:
        move expression, reg1
        jump not condition, label2
        语句
        jump label1
    label2:
        ...
    

    一些语言提供了 do-while语句的其他变体,例如 do-until (颠倒了条件表达式的含义).

    一些语言通过提供子句来提早终止循环迭代 (break 语句) 或跳过循环主体的其余部分并强制进行下一个循环迭代 ( continue 语句). ,从而实现更复杂的流控制。这些构造允许有时会出现 半循环 (loop-and-a-half)的情况(下面将进一步讨论)。

    一些语言允许使用 标记的 break 构造打破嵌套循环。 (这将在下面的示例 L-1和示例 N-1 中进一步讨论。)


    for 循环

    大多数结构化编程语言提供了更复杂的迭代构造,该构造允许计数器或数组索引随每次迭代递增或递减。此构造在功能上等同于 do-while 循环,但提供了循环迭代的控制实体(计数器或索引)的清晰意图:

    for i = 低值 to 高值 by 增量 do
        语句
    end
    

    这等效于以下使用goto的非结构化代码:

        i = 低值
    loop:
        if i > 高值
            goto endloop
        语句
        i = i + 增量
        goto loop
    endloop:
        ...
    

    这个结构可以被下面的机器码实现:

    # for-loop statement
        move 低值, reg1
    label1:
        cmp  reg1, 高值
        jump greater, label2
        语句
        add  增量, reg1
        jump label1
    label2:
        ...
    

    为了完全支持 for 循环,语言也应该处理负增量值。

    上面显示的 for 循环 的形式将循环主体迭代零次或多次。其他 for 循环 变体在循环体之后测试控制变量 (又叫 循环索引) 这使循环至少迭代一次。

    for 循环 的其他变体允许指定多个循环计数器(又叫循环索引)

    一个常见的编程问题是处理 半循环 (loop-and-a-half) 的构造,即必须在某个条件下在其循环主体的中间终止一个循环,以便在最后一次迭代期间不执行循环主体的某些部分。以下代码使用 break 语句执行此操作:

    for i = 低值 to 高值 by 增量 do
        语句1
        if 条件
            break
        语句2
    end
    

    某些语言提供特殊的语法,专门用于在循环主体的中间终止循环:

    for i = 低值 to 高值 by 增量 do
        语句1
    exit when 条件
        语句2
    end
    

    break 语句一样, exit when 子句允许执行循环主体的前半部分之后在主体的其余部分执行之前终止。两种形式都代替了显式使用goto,如以下代码:

    for i = 低值 to 高值 by 增量 do
        语句1
        if 条件
            goto endloop
        语句2
    end
    endloop:
        ...
    

    一个相关的编程问题是使循环执行循环主体的一部分,然后在发生某些情况时跳过循环的其余部分强制执行循环的下一次迭代。一些语言为此提供了一个 continue 语句:

    for i = 低值 to 高值 by 增量 do
        语句1
        if 条件
            continue
        语句2
    end
    

    这代替了显式使用一个 goto 语句,如下所示:

    for i = 低值 to 高值 by 增量 do
        语句1
        if 条件
            goto nextloop
        语句2
    nextloop:
    end
    


    其他途径

    一些结构化编程语言,包括 Modula-2,Modula-3,Oberon,Eiffel 和 Java,假设它们提供的其他流控制机制足以完成所有编程任务所以永远都不需要 goto 语句,因此它们根本不提供 goto 语句。在设计编程语言时,这并不总是一个好的假设。语言设计人员无法预见所有可能的编程方案,并且在常规控制结构之外提供“逃逸机制 (escape mechanism) ”可使程序员能够在需要时绕过语言所施加的语法限制进行编程。(下面将详细讨论流控制结构不足的一些示例。)

    还值得注意的是,有些编程语言根本不提供结构化的流控制构造。 许多 函数式编程语言 如 LISP, Scheme 和 Prolog,没有提供 if-then-else之外的,或者仅提供了形式非常简单的传统结构化流控制结构。 迭代在这些语言一般使用递归来实现,且实际上这些语言是专门为递归任务和数据结构量身定制的。 但是这类语言通常通过提供强大的 动态编程 (dynamic programming) 机制和极其灵活的 非均匀数据类型 (non-homogeneous data type) 来弥补他们缺乏的结构化流控制语句。


    第三部分

    Goto 还有使用必要性吗?

    良好的编程语言设计对一种语言提供足够完整且功能强大的流控制结构集,使得用这门语言为任何编程任务编写高效的代码相对容易。一种语言不应过分雄心勃勃,提供太多不同的方式来完成相同的事情,而与此同时,它也不应提供过少的表达的方式而表达能力不足。提供的流控制语句集应该足够强大和灵活,以便程序员可以清晰,简洁地表达自己的思想,而不必只是为了避开语言的语法限制而求助于使用无关的控制变量或不自然地重新布置他的代码。

    以下各节讨论了两个主要的编程问题,这两个问题传统上使用 goto 语句解决。这些问题是根据当前的编程技术提出的,目的是看看目前存在的编程语言是否先进到可在不使用 goto 的情况下处理这些问题。


    退出循环

    Dijkstra 呼吁完全消除 goto 语句的呼吁在理论上是可以的,但在实践中行得通吗?上面描述的控制流语句对于大多数编程逻辑而言已足够,但是在某些编程情况下,需要更强大的结构。

    goto 语句常用在于循环内尽早退出的情形,特别是如果必须从两层或更多层的嵌套循环内退出。C 语言提供了简单的循环逃逸机制,即 breakcontinue 语句。

    例 L-1 - 用 Break 退出循环
    // 一个要提前退出的简单循环
    
    for (;;)
    {
        int   ch;
    
        ch = read();
        if (ch == EOF)
            break;                  // With a loop escape
    
        parse(ch);
    }
    

    为了在不使用此类机制的情况下尽早退出循环,必须进行权衡,其中使用一个附加标志(布尔)变量来表示循环已完成。这会在每个循环的顶部有产生额外变量和测试该变量的额外开销。

    例 L-2 - 不用 Break 退出循环
    // 一个没有循环逃逸机制的简单循环 
    
    bool  incomplete = true;
    
    while (incomplete)
    {
        int   ch;
    
        ch = read();
        if (ch == EOF)
            incomplete = false;     // Without a loop escape
        else
            parse(ch);
    }
    

    退出更复杂的循环,即两个或更多层的嵌套循环,需要该语言进一步的支持。某些语言(例如 Java)提供了离开以标签名称为前缀的循环的功能。

    例 N-1 - 退出嵌套循环
    // [Java]
    //退出一个嵌套循环
    
    readLoop:
        for (;;)
        {
            char[]  line;
    
            line = readLine();
            if (line.length > 0)
            {
                for (int i = 0;  i < line.length;  i++)
                {
                    int   ch;
    
                    ch = line[i];
                    if (ch == '#')
                        break readLoop;     // 退出外层 for 循环 
    
                    parse(ch);
                }
            }
            else
                return;
        }
    

    其他语言(例如 C 和 C++)没有提供退出多层的循环嵌套的机制,因此必须使用 goto 语句。

    例 N-2 - 退出嵌套循环
    // [C / C++]
    // 在没有标签前缀的情况下退出一个嵌套循环
    
        for (;;)
        {
            char  line[80];
            int   len;
    
            len = readLine(line);
            if (len > 0)
            {
                for (int i = 0;  i < len;  i++)
                {
                    int   ch;
    
                    ch = line[i];
                    if (ch == '#')
                        goto endReadLoop;   // 退出外层 for 循环
    
                    parse(ch);
                }
            }
            else
                return;
        }
    endReadLoop:;
    

    这和Java代码一样干净,效率也很高。

    替代方法是使用一个额外的变量和一个额外的 if 语句来避免使用gotos,如示例 L-2.

    例 N-3 - 不用 goto 退出嵌套循环
    // [C / C++]
    //不用 goto 退出嵌套循环
    
        bool  notDone =  true;
    
        while (notDone)
        {
            char  line[80];
            int   len;
    
            len = readLine(line);
            if (len > 0)
            {
                for (int i = 0;  notDone  &&  i < len;  i++)
                {
                    int   ch;
    
                    ch = line[i];
                    if (ch == '#')
                        notDone = false;    // 退出外层 while 循环
                    else
                        parse(ch);
                }
            }
            else
                return;
        }
    


    异常处理

    goto 语句的另一个常见用法是处理 异常,或 Dijkstra 所谓的 终止语句。 某些语言(尤其是较新的面向对象的语言)提供了用于处理 同步 错误的 异常处理 机制,而较旧的语言则没有。

    下面的代码是 C 函数,该函数相当有效地利用 goto 语句进行错误处理和恢复。由于 C 没有任何类型的异常处理机制,因此精心设计的 goto 语句提供了合理的替代方案。

    考虑执行四个操作的一个过程:

    1. 分配控制对象。
    2. 保存指定文件名的副本。
    3. 打开命名文件。
    4. 从打开的文件中读取文件头块。

    以下用 C 编写的示例执行了这些操作:

    例 E-1 - 用 goto 处理错误
    // open_control() -- [C]
    // 打开一个文件并给他分配一个控制对象。
    // 成功时返回控制对象的指针, 失败时返回 NULL。
    
    struct Control * open_control(const char *fname)
    {
        struct Control *  ctl = NULL;
        FILE *            fp =  NULL;
    
        // 1. 分配一个控制对象
        ctl = malloc(sizeof(struct Control));
        memset(ctl, 0, sizeof(struct Control));
        if (ctl == NULL)                        // E-1
            goto fail;
    
        // 2. 储存文件名
        ctl->name = malloc(strlen(fname)+1);
        if (ctl->name == NULL)                  // E-2
            goto fail;
        strcpy(ctl->name, fname);
    
        // 3. 打开该名字的文件
        fp = fopen(fname, "rb");
        if (fp == NULL)                         // E-3
            goto fail;
        ctl->fp = fp;
    
        // 4. 读打开的文件的文件头块
        if (!read_header(ctl))                  // E-4
            goto fail;
    
        // 返回成功的结果 
        return ctl;
    
    fail:
        // 发生失败,释放分配的资源 
        if (ctl != NULL)
        {
            if (ctl->fp != NULL)
                fclose(ctl->fp);                // H-3
            if (ctl->name != NULL)
                free(ctl->name);                // H-2
            free(ctl);                          // H-1
        }
    
        // 返回失败的结果
        return NULL;
    }
    

    四个操作中的任何一个都可能失败,从而导致整个函数失败。但是,每次失败后,必须释放分配资源。因此,在 E-1 点发生故障需要在 H-1 点处有相应的清理代码,同样对于 E-2 和 E-3 发生故障也是如此。这些清除操作以与分配资源顺序相反的顺序执行。

    通常认为这种 goto 语句的使用是 goto 的“正确”使用。具体而言,至少对于不提供异常处理控制结构的语言(如 C)而言,通常认为将 goto 用于错误处理是可接纳的编程风格。

    但是,必须格外小心,使 goto 语句明显被用于错误处理,例如,为 goto 的标签选择适当的描述性名称。

    如果我们遵循 Dijkstra 的箴言并删除所有的 goto 语句,我们将得到类似以下的内容。

    例 E-2 - 无 goto 的错误处理
    // open_control() -- [C,版本2,没有goto]
    // 打开一个文件并给他分配一个控制对象。
    // 成功时返回控制对象的指针, 失败时返回 NULL。
    
    struct Control * open_control(const char *fname)
    {
        struct Control *  ctl = NULL;
        FILE *            fp =  NULL;
    
        // 1. 分配一个控制对象
        ctl = malloc(sizeof(struct Control));
        memset(ctl, 0, sizeof(struct Control));
        if (ctl == NULL)                        // E-1
        {
            // 失败,释放资源 
            return NULL;
        }
    
        // 2. 储存文件名
        ctl->name = malloc(strlen(fname)+1);
        if (ctl->name == NULL)                  // E-2
        {
            // 失败,释放资源
            free(ctl);                          // H-1
            return NULL;
        }
        strcpy(ctl->name, fname);
    
        // 3. 打开该名字的文件
        fp = fopen(fname, "rb");
        if (fp == NULL)                         // E-3
        {
            // 失败,释放资源
            free(ctl->name);                    // H-2
            free(ctl);                          // H-1
            return NULL;
        }
        ctl->fp = fp;
    
        // 4. 读打开的文件的文件头块
        if (!read_header(ctl))                  // E-4
        {
            // 失败,释放资源
            fclose(ctl->fp);                    // H-3
            free(ctl->name);                    // H-2
            free(ctl);                          // H-1
            return NULL;
        }
    
        // 成功 
        return ctl;
    }
    

    这种错误处理方式的问题在于,我们最终得到了很多重复的清理代码。

    这暗示了一种不使用 goto 但可以避免代码重复的替代形式。

    例 E-3 - 无 goto 的错误处理
    // open_control() -- [C,版本3, 无 goto]
    // 打开一个文件并给他分配一个控制对象。
    // 成功时返回控制对象的指针, 失败时返回 NULL。
    
    struct Control * open_control(const char *fname)
    {
        struct Control *  ctl = NULL;
        FILE *            fp =  NULL;
        int               err = 0;
    
        // 1. 分配一个控制对象
        ctl = malloc(sizeof(struct Control));
        memset(ctl, 0, sizeof(struct Control));
        if (ctl == NULL)                        // E-1
            err = 1;
    
        // 2. 储存文件名
        if (err == 0)
        {
            ctl->name = malloc(strlen(fname)+1);
            if (ctl->name == NULL)              // E-2
                err = 2;
            else
                strcpy(ctl->name, fname);
        }
    
        // 3. 打开该文件
        if (err == 0)
        {
            fp = fopen(fname, "rb");
            if (fp == NULL)                     // E-3
                err = 3;
            else
                ctl->fp = fp;
        }
    
        // 4. 读打开的文件的文件头块
        if (err == 0)
        {
            if (!read_header(ctl))              // E-4
                err = 4;
        }
    
        // 检测是否成功 
        if (err == 0)
            return ctl;
    
        // 失败,释放资源
        if (err > 3)
            fclose(ctl->fp);                    // H-3
        if (err > 2)
            free(ctl->name);                    // H-2
        if (err > 1)
            free(ctl);                          // H-1
    
        return NULL;
    }
    

    注意,如前所述,释放操作以与执行它们相应的分配操作相反的顺序执行。

    这种错误处理风格几乎与示例 E-1 使用的风格一样简洁明了。但是,它需要附加的错误指示符变量和附加的条件(if)语句。

    如果示例 E-1 的功能是用 C++ 编写的,则可以利用 C++ 支持异常处理机制(即 try-catch 语句)这一事实,从而使代码中的错误处理更加明显:

    Example T-1 - 无 goto 的错误处理
    // openControl() -- [C++]
    // 打开一个文件并给他分配一个控制对象。
    // 成功时返回控制对象的指针, 失败时返回 NULL。
    
    Control * openControl(const char *fname)
    {
        Control *   ctl =   NULL;
        FILE *      fp =    NULL;
    
        try
        {
            // 1. 分配一个控制对象
            ctl = new Control;
            if (ctl == NULL)                    // E-1
                throw 1;
    
            // 2. 储存文件名
            ctl->name = new char[::strlen(fname)+1];
            if (ctl->name == NULL)              // E-2
                throw 2;
            ::strcpy(ctl->name, fname);
    
            // 3. 打开该文件
            fp = ::fopen(fname, "rb");
            if (fp == NULL)                     // E-3
                throw 3;
            ctl->fp = fp;
    
            // 4. 读打开的文件的文件头块
            if (not ctl->readHeader())          // E-4
                throw 4;
    
            // 成功 
            return ctl;
        }
        catch (int err)
        {
            // 发生错误,释放资源 
            if (ctl != NULL)
            {
                if (ctl->fp != NULL)
                    ::fclose(ctl->fp);          // H-3
    
                if (ctl->name != NULL)
                    delete[] ctl->name;         // H-2
    
                delete ctl;                     // H-1
            }
    
            // 失败 
            return NULL;
        }
    }
    

    请注意,发生故障后清理所需的工作量与示例 E-1 完全相同。此外,显式使用 try-catch 语句使代码明显用于错误处理和恢复。

    由于 C++ 是一种面向对象的语言,因此允许程序员对对象的分配和释放进行更明确的控制,因此我们可以使 Control 对象的析构函数执行大多数清理操作。将 H-2 和 H-3 处的操作移到析构函数中,使我们可以编写一个更简单的 catch 语句来处理故障。

    Example T-2 - 无 goto 的错误处理
    // openControl() -- [C++, 版本2]
    // 打开一个文件并给他分配一个控制对象。
    // 成功时返回控制对象的指针, 失败时返回 NULL。
    
    Control * openControl(const char *fname)
    {
        Control *   ctl =   NULL;
        FILE *      fp =    NULL;
    
        try
        {
            ... 与例 T-1 相同 ...
        }
        catch (int err)
        {
            // 发生错误,释放资源
            delete ctl;                         // H-1, H-2, H-3
    
            // 失败
            return NULL;
        }
    }
    
    
    // Control::~Control() -- destructor
    
    Control::~Control()
    {
        // 释放分配的资源
        if (this->fp != NULL)
            ::fclose(this->fp);                 // H-3
        this->fp = NULL;
    
        if (this->name != NULL)
            delete[] this->name;                // H-2
        this->name = NULL;
    }
    

    虽然用 try-catch 的解决方案比使用 goto 的更干净,但它的缺点是管理 trycatch 语句需要更多的开销,这可能会非常昂贵。

    某些语言(例如 Java)把 finally 语句作为 try-catch 语句的一部分提供,以提供一种方法来指定不管是否发生异常都必须执行的操作。

    例 T-3 - finally 语句
    // [Java]
    
    void write3(Resource dest, Item[] data)
    {
        try
        {
            dest.acquire();
            dest.write(data[0]);
            dest.write(data[1]);
            dest.write(data[2]);
        }
        catch (ResourceException ex)
        {
            // 发生错误,释放资源
            log.error(ex);
            dest.reset();
        }
        finally
        {
            // 总是执行
            dest.release();
        }
    }
    

    其他语言,例如 Eiffel,提供了 retry 语句作为异常处理的一部分,以允许在捕获到异常之后(大概是在执行某些纠正措施之后)再次执行过程。


    总结

    Goto 之道

    显然,循环逃逸机制和异常处理语句使代码更具可读性也更高效。当然,这种流控制机制实际上只是变相的 goto 语句,是在底层使用机器码的 jump 指令实现的。 但它们不会破坏或混淆程序的执行状态。因此,根据 Dijkstra 的“可以确定地跟踪程序执行”的行为准则来判断的话,高级语言可以接纳它们这些控制流机制。

    T-2 和例 N-1 证明,只要 编程语言提供一组合理的控制结构来代替简单的 goto 语句,就可以满足 Dijkstra 的箴言。

    E-1 和例 N-2 证明,如果一种编程语言没有提供合理且强大的流控制结构,那么 只有 依靠使用 goto 语句才能合理地解决某些编程问题。

    一些结构化的编程语言根本不提供 goto 语句。诸如 Smalltalk,Eiffel 和 Java 之类的语言为早期和嵌套循环退出以及异常处理提供了控制语句,因此实际上并不需要 goto。一些其他语言(例如 Modula-2 和 Oberon)也未提供 goto 语句 ,但似乎也缺少足够的流控制结构,因此无法方便地编写提前退出循环和异常处理的代码。看上去这些语言是失败的语言实验,因为它们刻板地遵循了 Dijkstra 的箴言。

    Dijkstra 认为非结构化的 goto 语句不利于良好的编程的信念仍然是正确的。正确设计的语言应提供足够强大的流控制结构,以应对几乎所有编程问题。同样,当必须使用流控制语句不够灵活的语言时,程序员应在使用非结构化替代方案时保持克制。这就是 goto 之道:知道何时将其善用,知道何时不将其恶用。


    离别时,我忍不住给出 goto 语句的最后一个示例。我在一个较大的编译器项目(大约于1988年)中使用的LR解析器的库中遇到了此代码。这是一个简单精巧的编程凝结的非凡珍宝。

    最后一例 - 不可小觑的 Goto
    int parse()
    {
        Token   tok;
    
    reading:
        tok = gettoken();
        if (tok == END)
            return ACCEPT;
    shifting:
        if (shift(tok))
            goto reading;
    reducing:
        if (reduce(tok))
            goto shifting;
        return ERROR;
    }
    

    我把不使用 goto 语句重写这段代码留作读者的练习。


    参考文献

    1. Go To Considered Harmful
      Edsger W. Dijkstra
      Letter to Communications of the ACM (CACM)
      vol. 11 no. 3, March 1968, pp. 147-148.
      Online at: www.acm.org/classics/oct95

    2. Biography of Edsger W. Dijkstra
      born May 1930, died Aug 2002
      Wikipedia: en.wikipedia.org/wiki/Dijkstra

    3. Biography of C. A. R. (Tony) Hoare
      Wikipedia: en.wikipedia.org/wiki/C._A._R._Hoare

    4. Biography of Niklaus Wirth
      Wikipedia: en.wikipedia.org/wiki/Wirth
      Home page: www.cs.inf.ethz.ch/~wirth

    5. Goto programming statement
      Wikipedia: en.wikipedia.org/wiki/GOTO

    6. Formal Verification
      Wikipedia: en.wikipedia.org/wiki/Formal_verification

    7. Program Derivation
      Wikipedia: en.wikipedia.org/wiki/Program_derivation

    8. Hoare Logic
      Wikipedia: en.wikipedia.org/wiki/Hoare_logic

    9. An Axiomatic Basis For Computer Programming
      C. A. R. (Tony) Hoare
      Communications of the ACM (CACM)
      v.12 n.10, Oct 1969
      portal.acm.org/citation.cfm?doid=363235.363259 (subscription required)

    10. Programming Language Newsgroups
      groups.google.com/groups/dir?q=comp.lang.*


    This document is: http://david.tribble.com/text/goto.html.