断言的使用
断言,即assert。assert exp; 中,当exp表达式为false时,会终止程序。
注意,在java中,想让断言生效,jvm(VM options)要加"-ea"参数。
public static void main(String[] args) {
int a = (int) Math.sqrt(114514);
assert a == 1919810;
System.out.println(a);
}
因为a == 1919810为false,所以程序会终止。
传统写程序
- 写程序
(bug) - 写测试
- 一直跑测试直到通过所有测试(不等于没有bug)
相信大家也看出问题了,通过所有测试不等于没有bug。只代表了程序在这些测试上没有问题。而写测试也是一门艺术,会耗费很多的时间。
在大型项目中debug往往让人身心疲惫,在加上多线程(不能调试和概率性的出错),就更难了(鄙人写cmu15445的b+tree引索时,跑10次多线程测试,报2次错)。而且多线程代码往往涉及项目的核心。
奇思妙想
思考
我们为什么要写完bug,再去打补丁呢?或者说,我们能否在写代码的时候,就做好安全措施呢?
其实,我们只要在写代码的时候,用断言去验证代码需要遵守的规则(安全措施),就行了。
以拓扑排序为例子
拓扑排序框架
// 维护编号为1 ~ n的点集
import java.util.*;
public class Main {
public static void main(String[] args) {
init();
System.out.println(topSort(new ArrayList<>()));
}
static int n, m, idx;
static int[] h, ne, v, d;
// 添加边 a -> b
static void add(int a, int b) {
// TODO
}
// 返回是否有拓扑排序
static boolean topSort(int x, ArrayList<Integer> data) {
// TODO
}
// 初始化,节点编号是1~n
static void init() {
int nn = 7;
int mm = 8;
if (nn < 0 || mm < 0) {
throw new RuntimeException("size < 0 !!!");
}
n = nn;
m = mm;
idx = 0;
h = new int[n + 1];
d = new int[n + 1];
ne = new int[m + 1];
v = new int[ne.length];
Arrays.fill(h, -1);
// 测试数据
add(6, 3);
add(3, 4);
add(4, 1);
add(4, 2);
add(4, 5);
add(5, 2);
add(5, 7);
add(7, 2);
}
}
我们写的时候用assert添加规则。
添加边
static void add(int a, int b) {
assert a >= 1 && a <= n : "规则:点a编号范围[1, n]";
assert b >= 1 && b <= n : "规则:点b编号范围[1, n]";
v[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
assert d[b] >= 0 : "规则:入度d[b]大于等于0";
++d[b];
assert idx <= m : "规则:添加的边数一定小于总边数";
}
拓扑函数
下面的代码有一个经典的错误,你能看出来吗。看不出来也没关系,因为我们用assert对程序设置了安全措施。你可以运行一下看看,会怎么样。
static boolean topSort(ArrayList<Integer> data) {
if (data == null) {
throw new RuntimeException("data is null !!!");
}
Queue<Integer> q = new LinkedList<>();
for (int i = 1; i <= n; i++) {
assert i >= 1 && i <= n : "规则:点i编号范围[1, n]";
assert d[i] >= 0 : "规则:入度d[i]大于等于0";
if (d[i] == 0) {
q.offer(i);
}
}
while (!q.isEmpty()) {
int u = q.poll();
data.add(u);
assert u >= 1 && u <= n : "规则:点u编号范围[1, n]";
for (int i = h[u]; i != -1; i = ne[i]) {
int j = v[i];
assert j >= 1 && j <= n : "规则:点j编号范围[1, n]";
if (d[j]-- == 0) {
q.offer(j);
assert q.size() <= n : "规则:队列q大小小于等于n";
continue;
}
assert d[j] > 0 : "规则:当j未入队时,入度d[j]大于0";
}
}
if (data.size() == n) {
System.out.println(data);
return true;
}
return false;
}
结果:
Exception in thread "main" java.lang.AssertionError: 规则:当j未入队时,入度d[j]大于0
at Main.topSort(Main.java:25)
at Main.main(Main.java:8)
修复:
20c20
< if (--d[j] == 0) {
---
> if (d[j]-- == 0) {
新的流程
- 写(改)代码时,再做一些小小的工作。
- 修复出错的规则,循环。
总结
这正如许多家庭主妇所了解的,在日常生活中付出一点防患于未然的代价,往往能在长期内不断收到回报。受其启发,为了降低出bug的概率,我们使用断言对程序的额外维护操作——在每次写代码中,设置了一系列的规则,限制了某些程序员放飞自我。这种方法仅仅稍许增加了写代码的时间,同时其规则的编写是非常简单的。
这一小小改变所带来的好处,更多地表现为长期的改善,而非眼前的利益。
最后
所以,当你下次写程序懒得思考规则有了奇怪的bug,无法修复的时候。要记住,这就是懒虫的下场,活该 ( : 。