14

最后,我有一个关于 Stack Overflow 的问题要问!:-)

主要目标是 Java,但我相信它主要与语言无关:如果您没有本机断言,您总是可以模拟它。

我在一家销售一套用 Java 编写的软件的公司工作。代码很旧,至少可以追溯到 Java 1.3,并且在某些地方,它显示... 这是一个很大的代码库,大约 200 万行,所以我们不能一次全部重构。
最近,我们将最新版本从 Java 1.4 语法和 JVM 切换到 Java 1.6,保守地使用了一些新功能,例如assert(我们曾经使用 DEBUG.ASSERT 宏——我知道assert在 1.4 中已经引入但我们没有使用它之前)、泛型(仅类型化集合)、foreach 循环、枚举等。

尽管我已经阅读了几篇关于该主题的文章,但我对 assert 的使用仍然有些不满。然而,我看到的一些用法让我感到困惑,伤害了我的常识...... ^_^ 所以我想我应该问一些问题,看看我想要纠正的东西是否正确,或者它是否违反了惯例。我很罗嗦,所以我把问题加粗了,给那些喜欢略读的人。

作为参考,我在 SO中搜索了assert java并发现了一些有趣的线程,但显然没有完全相同的重复。

首先,主要问题,今天引发了我的问题:

SubDocument aSubDoc = documents.GetAt( i );
assert( aSubDoc != null );
if ( aSubDoc.GetType() == GIS_DOC )
{
   continue;
}
assert( aSubDoc.GetDoc() != null );
ContentsInfo ci = (ContentsInfo) aSubDoc.GetDoc();

是的,我们使用 MS 的 C/C++ 样式/代码约定。我什至喜欢它(来自相同的背景)!所以起诉我们。
首先,assert()表单来自DEBUG.ASSERT()调用的转换。我不喜欢多余的括号,因为 assert 是一种语言结构,而不是(不再是)函数调用。我也不喜欢return (foo);:-)
接下来,断言不在这里测试不变量,它们被用作防止错误值的保护。但据我了解,它们在这里毫无用处:断言将引发异常,甚至没有用伴随字符串记录,并且只有在启用断言的情况下。所以如果我们有-ea选项,我们只是抛出一个断言而不是常规的 NullPointerException 一个。这看起来并不是最重要的优势,因为无论如何我们都会在最高级别捕获未经检查的异常。
我是否正确地假设我们可以摆脱它们并忍受它(即让 Java 引发这种不受约束的异常)?(或者,当然,如果可能的话,测试空值,这是在其他地方完成的)。

旁注:如果我必须在上面的代码段中断言,我会针对 ci 值而不是针对 getter 执行此操作:即使大多数 getter 都经过优化/内联,我们也无法确定,因此我们应该避免调用它两次。

有人告诉,在最后一个引用的线程中,公共方法应该使用针对参数值的测试(公共 API 的使用),而私有方法应该依赖于断言。好建议。
现在,这两种方法都必须检查另一个数据源:外部输入。IE。例如,来自用户、数据库、某个文件或网络的数据。
在我们的代码中,我看到了针对这些值的断言。我总是将这些更改为真实测试,因此即使禁用断言它们也会起作用:这些不是不变的,必须正确处理。
我只看到一个可能的异常,其中输入应该是常量,例如一个数据库表,其中填充了关系中使用的常量:如果这个表被更改但相应的代码没有更新,程序会中断。
您是否看到其他例外情况?

我看到的另一个相对频繁的用法,看起来还不错:在开关的默认值中,或者在一系列 else if测试所有可能值的末尾(这些情况可以追溯到我们使用枚举之前!),通常有一个assert false : "Unexpected value for stuff: " + stuff;
Looks legal for我(这些情况不应该在生产中发生),你怎么看?(除了这里不相关的“不切换,使用 OO”建议之外)。

最后,我在这里错过了任何其他有用的用例或烦人的陷阱吗?(大概!)

4

4 回答 4

10

第一条规则是避免断言中的副作用。换句话说,代码在关闭断言时的行为应该与打开断言且未失败时的行为相同(显然,失败的断言将改变行为,因为它们会引发错误)。

第二条规则是不要将断言用于必要的检查。它们可以关闭(或者,更准确地说,不打开)。对于非私有方法的参数检查,请使用 IllegalArgumentException。

断言是可执行的假设。我使用断言来陈述我对程序当前状态的信念。例如,诸如“我假设 n 在这里是正数”“我假设列表在这里只有一个元素”之类的东西。

于 2008-12-16T00:20:40.927 回答
3

我使用assert,不仅用于参数验证,还用于验证线程。每次我摇摆时,我几乎在每个方法中都写了断言来标记“我应该只在工作线程/AWTThread 中执行”。(我认为 Sun 应该为我们这样做。)由于 Swing 线程模型,如果您从非 UI 线程访问 swing api,它可能不会失败(并且随机失败)。没有断言很难找出所有这些问题。

我可以想象的另一个例子是检查 JAR 封闭的资源。您可以有英语例外而不是 NPE。


编辑:另一个例子;对象锁检查。如果我知道我将使用嵌套同步块,或者当我要修复死锁时,我会使用Thread.holdLock(Object)来确保不会以相反的顺序获得锁。


编辑(2):如果你很确定某些代码块永远不会到达,你可以写

throw new AssertionError("You dead");

而不是

assert false:"I am lucky";

例如,如果您在可变对象上覆盖“equals(Object)”,如果您认为它永远不会成为关键,则使用 AssertionError 覆盖 hashCode()。这种做法在一些书籍中有所建议。我不会损害性能(因为它永远不应该达到)。

于 2008-12-15T16:16:50.043 回答
2

您已经谈到了我认为通常应该避免断言的许多原因。除非您使用的代码库中断言的使用具有非常严格的指导方针,否则您很快就会陷入无法关闭断言的情况,在这种情况下,您还不如只使用正常的逻辑测试。

所以,我的建议是跳过断言。不要坚持额外的空指针检查语言会为你做的地方。但是,如果指针可能暂时不会被取消引用,那么预先进行空值检查是一个好主意。此外,对于“永远不会”发生的情况(最终的 if 分支或默认的 switch 情况),请始终使用真正的异常,不要使用“assert false”。如果您使用断言,则有可能有人可以将其关闭,如果情况确实发生,事情会变得非常混乱。

于 2008-12-15T15:58:47.413 回答
2

我建议检查公共 (API) 方法中的参数,如果参数无效则抛出 IllegalArgumentException。这里没有断言,因为 API 用户需要获取正确的错误(消息)。

断言应在非公共方法中用于检查后置条件和可能的前置条件。例如:

List returnListOfSize(int size) {
    // complex list creation
    assert list.size == size;
}

通常使用巧妙的错误处理策略可以规避断言。

于 2008-12-15T15:59:02.840 回答