系列名称:如何像计算机科学家一样思考(How to think like a computer scientist)
包含版本:C++、JAVA、Python
我很喜欢这种教育方式~
附录A:程序开发计划
如果花费了大量的时间在调试上,很可能是因为没有一个有效的程序开发计划。
一个典型的不好的程序开发计划就像这样:
1. 编写一个完整的方法。
2. 编写更多的方法。
3. 编译程序。
4. 花一个小时来找语法错误。
5. 花一个小时来找运行时错误。
6. 花三个小时来找语义错误。
显而易见,问题出在头两步。如果写了一个方法甚至很多方法都不调试,那么得到的代码可能已经多得让你无法调试了。 如果遇到这种情况,唯一的解决办法就是删除代码直到再次获得一个可以工作的程序,然后再慢慢将程序增加回来。编程新手往往不希望这么干,因为他们精心编写的代码实在是太宝贝了。可是为了高效的进行调试,你不得不残忍起来!
下面是一个较好的程序开发计划:
1. 从一个能做一些直观事情(比如打印一些东西)的程序开始。
2. 每次增加少许几行代码,并且每次改动都测试程序是否正确。
3. 重复前两步直到程序满足预期的要求。
每次改动后的程序都应该产生一些验证新添代码的可见效果。这种编程方式能节省许多时间。因为一次只增加少许几行代码,所以容易发现语法错误;程序的每个版本产生一些可见的结果,这就使你能不断测试自己头脑中关于程序是如何工作的模型。如果头脑中的模型是错的,在写出一大堆错误代码之前你将面对矛盾(并且也有了改正的机会) 。
这种方式的问题是常常难于找出下手的地方并得到一个完整正确的程序。我将通过开发一个名为isIn 的方法来演示这种方式。 这个方法取一个字符串和一个字符为参数, 返回一个布尔值: 如果字符出现在字符串中就返回 true否则返回 false。
1. 第一步,写一个尽量短但可以编译、运行并做一些可见的事情的方法:
|
当然,要测试这个方法就要调用它,需要在main 方法或在一个正常工作的程序中什么地方创建一个简单的测试用例。 先来看一个字符串中出现了字符的用例(期望得到的结果是true) :
|
如果一切按照计划进行,代码将顺利编译、运行然后打印单词 isIn 和值false。当然,答案是不对的,但目前我们知道方法被调用并且返回了值。 在个人的编程生涯里,我浪费了太多太多的时间调试某个方法,结果却发现它根本没有被调用。如果当时我采用这种开发方式,这种事情是不应该发生的。
2. 第二步,检查方法取得的参数:
|
第一个打印语句允许我们确认 isIn 方法找的是正确的字母, 第二句用来确认找的是正确的位置。
现在输出是:
isIn looking for b
in the String banana
既然已经知道它们的作用,再打印参数似乎有点傻。关键在于确认它们是否和我们设想的一样。
3. 为了遍历字符串,可以利用 7.3 节的代码。一般来说,重用代码片断比全部从头开始更好。
|
现在运行这个程序,它将一次打印字符串中的一个字符。如果一切进展良好,可以确定这个循环检测了字符串中的每个字母。
4. 到这里还没有充分思考过这个方法到底要做什么, 此时我们最需要的可能就是找到一种算法。最简单的算法是一个线性查找,即遍历向量并将每个元素和目标键进行比较。
令人愉快的是前面已经写过了遍历向量的代码。和以往一样,每次增加几行:
|
遍历字符串的时候,将每一个字母和目标键做比较。如果找到了目标,就打印一些信息, 这样我们就能知道新增代码执行的时候产生了一些可见的效果。
5. 现在已经很接近正确工作的代码了。 如果找到了要找的内容那么下一个改动是要从方法中返回:
|
如果找到了目标字符,返回true;如果经历了整个循环也没有找到,正确的返回值就应该是false。
现在运行程序应该得到:
isIn looking for n
in the String banana
b
a
n
found it
true
6. 下一步是要确认别的测试用例能正确的工作。 首先应该确认如果字符没有在字符串中则方法返回false。然后要检查一些典型的容易招致麻烦的情况,例如空字符串”",或者只有一个字符的字符串。
一般说来这种测试可以帮我们找到存在的bug,但不能判断方法是否正确。
7. 倒数第二步要移出或注释掉打印语句。
|
如果稍后还要查看这个方法,那么将打印语句注释掉是一个好主意。但如果这个方法是最终版本并且你能确信它是正确无误的,就可以移出这些打印语句了。
移出注释可以让代码更加干净,也有助于发现遗留的问题。 如果代码的用意并不是很明显,就应该增加注释来解释清除。要抵制逐行翻译代码的诱惑。例如,这样做是毫无必要的:
// if letter equals c, return true
if (letter == c) {
return true;
}
注释应该用来解释含义不明显的代码,提示容易导致错误的情况和说明包含在代码中的假设。还有,在每个方法之前给出该方法的用途也是一个很好的做法。
8. 最后一步是检测代码并确认它是正确的。在这里我们知道方法的语法是正确的,因为编译顺利通过。要检查运行时错误,只有找出每个可能导致错误的语句和条件。
在这方法中唯一能导致运行时错误的语句是 s.charAt (index)。如果s 是null 或者索引超越了边界那么这条语句就将失败。因为s 是一个参数,就不能确保它不是null,所以只有检查。一般说来方法最好都要确认参数的合法性。while 循环的结构保证了 index 总是在0 到 s.length-1 之间。如果检查全部有问题的条件,或者证明这些条件不可能发生,那么就证明了方法不会导致运行时错误。
我们还没有证明这个方法的语义是正确的,但在逐步递增的过程中,避免了很多可能的错误。例如已经知道方法能正确取得参、循环遍历了整个字符串。我们还知道这个方法成功的比较了字符,如果目标在字符串中就返回true。最后我们知道,如果循环存在就表明目标不在字符串中。
在没有正式证明的情况下,这可能是我们能做到的最好情况。