使用assert(Java等)的一些(反)模式

最后,我有一个问题要问Stack Overflow! 🙂

主要目标是Java,但我认为它主要与语言无关:如果你没有本机断言,你可以随时模拟它。

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

尽管我已经阅读了几篇关于这个主题的文章,但我对assert的使用仍然有点青睐。 然而,我看到的一些用法让我感到困惑,伤害了我的常识… ^ _ ^所以我想我应该问一些问题,看看我是否正确想要纠正的东西,或者它是否违反常规做法。 我很啰嗦,所以我为那些喜欢浏览东西的人提出了问题。

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

  • 如何避免java中的“!= null”语句? 以及多少空检查就足够了? 非常相关,因为很多断言我们只检查变量是否为空。 在我们的代码中的某些地方,有空对象的用法(例如,返回new String[0] )但并非总是如此。 我们必须忍受这一点,至少是为了维护遗留代码。
  • Java声明中的一些好的答案也未得到充分利用 。
  • 哦,SO表明我什么时候应该使用Debug.Assert()? 问题也是相关的(减少重复的好function!)。

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

 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()forms来自DEBUG.ASSERT()调用的转换。 我不喜欢额外的括号,因为assert是一个语言结构,而不是(不再是,这里)一个函数调用。 我也不喜欢return (foo); 🙂
接下来,断言不在这里测试不变量,它们更像是用来防止坏值。 但正如我所理解的那样,它们在这里毫无用处:assert将抛出一个exception,甚至没有记录伴随字符串,并且只有在启用断言时才会抛出exception。 因此,如果我们有-ea选项,我们只会抛出一个断言,而不是常规的NullPointerException。 这看起来不是最重要的优势,因为无论如何我们都会在最高级别捕获未经检查的exception。
我是否正确地假设我们可以摆脱它们并与之共存(即让Java引发这种无法解决的exception)? (或者,当然,如果可能的话,测试空值,这在其他地方进行)。

旁注:我应该在上面的片段中断言,我会针对ci值而不是针对getter:即使大多数getter被优化/内联,我们也不能确定,所以我们应该避免调用它两次。

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

我看到的另一个相对频繁的用法,似乎没问题:在交换机的默认情况下,或者在测试所有可能的值(这些情况可以追溯到我们使用枚举之前!)的一系列else if结尾时,通常有一个assert false : "Unexpected value for stuff: " + stuff;
对我来说看起来合法(这些情况不应该在生产中发生),你怎么看? (除了“无开关,使用OO”这些与此无关的建议)。

最后, 还有其他有用的用例或我在这里错过的讨厌的陷阱吗? (大概!)

最重要的规则是避免断言中的副作用。 换句话说,代码应该与关闭断言的行为完全相同,因为断言打开时没有失败(显然失败的断言会改变行为,因为它们会引发错误)。

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

断言是可执行的假设。 我使用断言表明我对该程序当前状态的看法。 例如,像“我假设n在这里是正面的”“我假设列表恰好有一个元素在这里”之类的东西

你已经触及了我认为应该避免断言的许多原因。 除非你正在使用一个代码库,其中assert使用具有非常严格的指导原则,你很快就会陷入无法关闭断言的情况,在这种情况下你可能只是使用正常的逻辑测试。

所以,我的建议是跳过断言。 不要坚持额外的空指针检查,语言将为你做。 但是,如果指针可能暂时不被解除引用,则前期空检查是个好主意。 另外,对于应该“永不”发生的情况(最终的if分支或默认的开关情况), 总是使用真正的例外,不要使用“断言错误”。 如果你使用断言,那么有可能有人将其关闭,如果情况真的发生,事情就会变得非常混乱。

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

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

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

通常使用聪明的error handling策略断言可以被规避。

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

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


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


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

 throw new AssertionError("You dead"); 

而不是

 assert false:"I am lucky"; 

例如,如果在可变对象上覆盖“equals(Object)”,则如果您认为它永远不是键,则使用AssertionError覆盖hashCode()。 一些书中提出了这种做法。 我不会伤害表现(因为它永远不会达到)。