在上一篇文章里,我们尝试用演绎推理的方式,证明了一个程序的正确性。
示例中的程序非常简单,但是证明的过程已经比较复杂了,需要相当的技巧。可以想见,如果要对实际项目里的程序做正确性证明,难度会有多高。那怎么把这件事简化一下呢?主要有两种思路,一种是只证明程序的一部分是正确的,也就是说,只针对程序里的一些片段做证明,而不是针对整个程序。我们平常通过代码走查,明确程序里哪些地方肯定没问题,进而缩小后面测试的关注范围,就是这个思路。
另一种思路,是只证明“程序于期望的一部分而言是正确的”。什么意思呢?我们来看一个例子。假设被测程序的期望是:“对给定的数组,按元素从小到大的顺序重新排序”。
这是程序的实现:
public static int[] sort(int[] arr) {
int temp = 0;
for(int i = 0; i <arr.length - 1; i++) {
for(intj = 0; j < arr.length-1-i; j++) {
if(arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
}
这个程序可比算阶乘的程序复杂多了,证明起来会非常困难。
怎么办呢?我们把原始期望F做一次冗余分解,得到两个子期望:
F1的意思是“输出数组中的元素是从小到大排序的”,F2的意思是“输出数组是输入数组的重新排序”。我们只证明F2成立。这就很容易了,因为程序里除了交换两个元素的位置之外,没有对输入数组做任何其它的修改。
那只证明F2成立有什么用呢?不能证明F成立,到头来还是要用测试用例来验证。
但是,如果证明了F2成立,就可以大幅降低测试用例的执行成本。假设我们选了一个“包含10000个元素的数组”当测试用例,怎么做结果校验呢?我们需要做两件事:
-
第一,我们要看输出数组里的元素是不是按从小到大排序的;
-
第二,要看输出数组里的10000个元素,是不是跟输入数组里的元素一一对应。
显然,第二件事要比第一件事麻烦得多。但是,如果我们已经证明了F2成立,第二件事就不用做了。这样,测试结果校验就变得很容易。可见有的时候,只证明“程序于期望的一部分而言是正确的”,仍然是很有意义的一件事。这就是演绎推理在工程实践中的价值。
我们讨论过,形式化证明并非解决正确性判定问题的灵丹妙药。已经被证明是“正确”的程序,仍然有可能出现错误。原因就在于,形式化证明往往需要基于一些简化设定。
形式化证明并非解决正确性判定问题的灵丹妙药。究其原因,是被测对象内在与外在的复杂性,致使我们对理想与现实进行形式化的过程中,难免引入各种简化设定。https://blog.csdn.net/wkqyxyh/article/details/149275881?spm=1011.2415.3001.5331如果我们只在理想与现实的“局部区域”应用演绎推理,就可以减少简化设定的引入,从而在一定程度上回避这种方法的局限性。