2.7 调试

当其他所有方法都失败时,调试是最后的手段。让我们退后一步,在调试之前思考一下所有的事情。

防范漏洞

根据 Rob Miller 的说法,有四种防范漏洞的方法:

  1. 防范漏洞的第一个方法是让它们变得不可能。

    通过选择保证内存安全(除非通过对该内存区域有效的指针(或引用),否则内存的任何部分都不能被访问)和类型安全(任何值的使用方式都不能与其类型不一致)的编程语言,可以消除整个类的错误。例如,OCaml 类型系统可以防止程序发生缓冲区溢出和无意义操作(比如向浮点数添加布尔值),而 C 类型系统则不能。

  2. 防范漏洞的第二种方法是使用发现漏洞的工具。

    有自动化的源代码分析工具,比如 FindBugs,它可以发现 Java 程序中许多常见的错误,还有 SLAM,它用来发现设备驱动程序中的错误。形式化方法是计算机科学(CS)的一个子领域,研究如何使用数学来指定和验证程序,即如何证明程序没有错误。我们将在本课程的后面学习验证。

    代码评审和结对编程等社会化方法也是查找错误的有用工具。IBM 在20世纪 70 – 90 年代的研究表明,代码审查可以非常有效。在一项研究中 (Jones, 1991),代码审查发现了 65% 的已知编码错误和 25% 的已知文档错误,而测试只发现了 20% 的编码错误,而且没有发现文档错误。

  3. 防范漏洞的第三种方法是让它们立即可见。

    错误出现得越早,就越容易诊断和修复。如果计算超过了错误点,那么进一步的计算可能会掩盖真正发生故障的位置。源代码中的断言 使程序“快速失败”和“明显失败”,这样错误就会立即显现,程序员就能准确地知道在源代码中的什么地方查找。

  4. 防范漏洞的第四种方法是广泛的测试。

    如何知道一段代码是否有特定的错误?编写测试来暴露这个错误,然后确认你的代码不会通过这些测试。对于相对较小的代码段 (如单个函数或模块),在开发该代码的同时编写单元测试尤其重要。这些测试的运行应该是自动化的,这样如果你破坏了代码,可以尽快发现。(这又是防范漏洞3。)

在所有这些防范措施都失败之后,程序员不得不求助于调试。

如何调试

你发现了一个错误。接下来你将怎么做?

  1. 将错误提取到一个小的测试用例中。 调试是一项艰苦的工作,但是测试用例越小,你就越有可能将注意力集中在隐藏错误的代码段上。因此,将时间花在此提取上可以节省时间,因为不需要重新阅读大量代码。在有一个小的测试用例之前不要继续调试!

  2. 采用科学的方法。 提出一个关于错误发生原因的假设。你甚至可以把假设写在笔记本上,就像你在化学实验室里一样,在自己的头脑中澄清它,并记录已经考虑过的假设。接下来,设计一个实验来确认或否定这个假设。运行你的试验并记录结果。根据你所学到的,重新制定你的假设。继续,直到理性地、科学地确定了错误的原因。

  3. 修复错误。 修复可能是一个简单的错误更正。或者它可能会暴露出一个设计缺陷,导致你做出重大更改。考虑是否需要将修复程序应用到代码库中的其他位置——例如,这是一个复制粘贴错误吗?如果是这样,是否需要重构代码?

  4. 将小测试用例永久地添加到测试套件中。 你不希望这个错误回到代码库中。因此,通过将它作为单元测试的一部分来跟踪这个小测试用例。这样,将来任何时候进行更改时,都将自动防止出现相同的错误。反复运行从以前的错误中提炼出来的测试是回归测试的一部分。

OCaml中的调试

这里有一些关于如何在 OCaml 中调试的技巧(如果你不得不这么做的话)。

  • 打印语句。插入一条打印语句以确定变量的值。假设你想知道 x 在以下函数中的值:

    let inc x = x + 1
    let inc x = x + 1
    let inc x = x + 1

    Just add the line below to print that value:

    只需要添加下面这行代码来打印这个值:

    let inc x =
    let () = print_int x in
    x + 1
    let inc x =
      let () = print_int x in
      x + 1
    let inc x = let () = print_int x in x + 1
  • 函数跟踪。 假设你想查看一个函数的递归调用和返回的跟踪。使用 #trace 指令:

# let rec fib x = if x <= 1 then 1 else fib (x - 1) + fib (x - 2);;
# #trace fib;;
  # let rec fib x = if x <= 1 then 1 else fib (x - 1) + fib (x - 2);;
  # #trace fib;;
# let rec fib x = if x <= 1 then 1 else fib (x - 1) + fib (x - 2);; # #trace fib;;

If you evaluate fib 2, you will now see the following output:

如果你计算 fib 2,你现在会看到以下输出:

fib <-- 2
fib <-- 0
fib --> 1
fib <-- 1
fib --> 1
fib --> 2
  fib <-- 2
  fib <-- 0
  fib --> 1
  fib <-- 1
  fib --> 1
  fib --> 2
fib <-- 2 fib <-- 0 fib --> 1 fib <-- 1 fib --> 1 fib --> 2

要停止跟踪,请使用 #untrace 指令。

  • 调试器。 OCaml 有一个调试工具 ocamldebug。你可以在 OCaml 网站上找到 教程。除非你使用 Emacs 作为编辑器,否则你可能会发现这个工具比仅仅插入打印语句更难使用。

防范性程序设计

正如我们在前面的调试部分中所讨论的,防止错误的一种方法是使任何漏洞(或错误)立即可见。这个概念和前置条件概念相联系。

考虑一下 random_int 的规范:

(** [random_int bound] 是一个介于 0 (包括)[bound] (不包括)之间的随机整数。
要求: [bound] 大于 0 且小于230次方。*)
(** [random_int bound] 是一个介于 0 (包括)和 [bound] (不包括)之间的随机整数。
  要求: [bound] 大于 0 且小于2的30次方。*)
(** [random_int bound] 是一个介于 0 (包括)和 [bound] (不包括)之间的随机整数。 要求: [bound] 大于 0 且小于2的30次方。*)

如果 random_int 的客户端传递了一个违反 “Requires” 子句的 bound 值,例如 -1 ,则 random_int 的实现可以自由地执行任何操作。当客户违反前置条件时,所有的备选都将作废。

random_int 能做的最有帮助的事情,是立即指出违反了前置条件。毕竟,客户端可能并不是有意违反它。

因此 random_int 的实现者最好检查是否违反了前置条件,如果违反了,就抛出异常。以下是这种防范性编程 的三种可能:

(* possibility 1 *)
let random_int bound =
assert (bound > 0 && bound < 1 lsl 30);
(* proceed with the implementation of the function *)
(* possibility 2 *)
let random_int bound =
if not (bound > 0 && bound < 1 lsl 30)
then invalid_arg "bound";
(* proceed with the implementation of the function *)
(* possibility 3 *)
let random_int bound =
if not (bound > 0 && bound < 1 lsl 30)
then failwith "bound";
(* proceed with the implementation of the function *)
(* possibility 1 *)
let random_int bound =
  assert (bound > 0 && bound < 1 lsl 30);
  (* proceed with the implementation of the function *)

(* possibility 2 *)
let random_int bound =
  if not (bound > 0 && bound < 1 lsl 30)
  then invalid_arg "bound";
  (* proceed with the implementation of the function *)

(* possibility 3 *)
let random_int bound =
  if not (bound > 0 && bound < 1 lsl 30)
  then failwith "bound";
  (* proceed with the implementation of the function *)
(* possibility 1 *) let random_int bound = assert (bound > 0 && bound < 1 lsl 30); (* proceed with the implementation of the function *) (* possibility 2 *) let random_int bound = if not (bound > 0 && bound < 1 lsl 30) then invalid_arg "bound"; (* proceed with the implementation of the function *) (* possibility 3 *) let random_int bound = if not (bound > 0 && bound < 1 lsl 30) then failwith "bound"; (* proceed with the implementation of the function *)

第二种可能对客户端来说信息量最大,因为它使用内置函数 invalid_arg 来引发命名良好的异常 Invalid_argument。事实上,这正是该函数的标准库实现所做的。

第一种可能在调试自己的代码时最有用,而不是选择将失败的断言暴露给客户端。

第三种可能性与第二种可能性的区别仅在于引发的异常的名称 (Failure)。当前置条件涉及不只一个无效参数时,它可能很有用。

在这个例子中,检查前置条件的计算成本很低。在其他情况下,它可能需要大量的计算,因此函数的实现者可能不喜欢检查前置条件,或者只检查与之近似的一些开销不大的东西。

有时,程序员会不必要地担心防御性编程的开销太大 —— 要么是在最初实现检查的时间上,要么是在检查断言时要付出的运行时成本上。这些担忧往往是多余的。社会为修复软件错误所花费的时间和金钱表明,我们都可以负担得起运行稍微慢一点的程序。

最后,实现者甚至可以选择消除前置条件,并将其重申为后置条件:

(** [random_int bound]是一个介于0(包括)[bound](不包括)之间的随机整数。
Raises: [Invalid_argument "bound"],除非[bound]大于0且小于2^30。*)
(** [random_int bound]是一个介于0(包括)和[bound](不包括)之间的随机整数。
  Raises: [Invalid_argument "bound"],除非[bound]大于0且小于2^30。*)
(** [random_int bound]是一个介于0(包括)和[bound](不包括)之间的随机整数。 Raises: [Invalid_argument "bound"],除非[bound]大于0且小于2^30。*)

现在,当 bound 太大或太小时,random_int 不能随意执行任何操作,而是必须抛出异常。对于这个函数来说,这可能是最好的选择。

在本课程中,我们不会强制你进行防御性编程。但如果你够聪明,你还是会开始 (或继续) 做这件事。花在编写此类防范措施上的少量时间将为你节省数小时的调试时间,让你成为更高效的程序员。

注:本书是康奈尔大学 CS 3110 数据结构和函数式编程的教材。原书为英文版,在学习的过程中,根据自己的理解,翻译了一些,做一个记录,版权归原作者所有,如有侵权,请联系我删除。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYBgLmqc' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片