断言的使用

断言,即assert。assert exp; 中,当exp表达式为false时,会终止程序。

注意,在java中,想让断言生效,jvm(VM options)要加"-ea"参数。

assert-1.png

public static void main(String[] args) {
        int a = (int) Math.sqrt(114514);
        assert a == 1919810;
        System.out.println(a);
}

因为a == 1919810为false,所以程序会终止。

传统写程序

  1. 写程序 (bug)
  2. 写测试
  3. 一直跑测试直到通过所有测试(不等于没有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) {

新的流程

  1. 写(改)代码时,再做一些小小的工作。
  2. 修复出错的规则,循环。

总结

这正如许多家庭主妇所了解的,在日常生活中付出一点防患于未然的代价,往往能在长期内不断收到回报。受其启发,为了降低出bug的概率,我们使用断言对程序的额外维护操作——在每次写代码中,设置了一系列的规则,限制了某些程序员放飞自我。这种方法仅仅稍许增加了写代码的时间,同时其规则的编写是非常简单的。
这一小小改变所带来的好处,更多地表现为长期的改善,而非眼前的利益。

最后

所以,当你下次写程序懒得思考规则有了奇怪的bug,无法修复的时候。要记住,这就是懒虫的下场,活该 ( : 。