有哪些真实的例子来理解断言的关键作用?


当前回答

断言允许检测代码中的缺陷。您可以打开断言进行测试和调试,而在程序处于生产状态时关闭断言。

既然你知道它是真的,为什么还要坚持呢?只有当一切都正常工作时,这才是正确的。如果程序有一个缺陷,它实际上可能不是真的。在过程早期检测到这一点可以让您知道哪里出了问题。

assert语句包含此语句以及可选的String消息。

assert语句的语法有两种形式:

assert boolean_expression;
assert boolean_expression: error_message;

下面是一些基本规则,它们控制着断言应该在哪里使用,不应该在哪里使用。断言应该用于:

验证私有方法的输入参数。不是公共方法。当传递坏参数时,公共方法应该抛出常规异常。 在程序的任何地方来确保一个事实的有效性,而这个事实几乎肯定是正确的。

例如,如果你确定它只会是1或2,你可以使用这样的断言:

...
if (i == 1)    {
    ...
}
else if (i == 2)    {
    ...
} else {
    assert false : "cannot happen. i is " + i;
}
...

在任何方法结束时验证后置条件。这意味着,在执行业务逻辑之后,您可以使用断言来确保变量或结果的内部状态与您所期望的一致。例如,打开套接字或文件的方法可以在末尾使用断言来确保确实打开了套接字或文件。

断言不应该用于:

验证公共方法的输入参数。由于断言可能并不总是被执行,因此应该使用常规异常机制。 对用户输入的内容验证约束。同上。 不应用于副作用。

例如,这不是一个正确的用法,因为断言在这里被用于调用doSomething()方法的副作用。

public boolean doSomething() {
...    
}
public void someMethod() {       
assert doSomething(); 
}

唯一可以证明这一点的情况是,当你试图找出断言是否在你的代码中启用时:

boolean enabled = false;    
assert enabled = true;    
if (enabled) {
    System.out.println("Assertions are enabled");
} else {
    System.out.println("Assertions are disabled");
}

其他回答

断言是用于捕获代码中的错误的开发阶段工具。它们被设计为易于删除,因此它们不会存在于生产代码中。因此断言不是您交付给客户的“解决方案”的一部分。它们是内部检查,以确保你所做的假设是正确的。最常见的例子是测试是否为空。很多方法都是这样写的:

void doSomething(Widget widget) {
  if (widget != null) {
    widget.someMethod(); // ...
    ... // do more stuff with this widget
  }
}

在这样的方法中,小部件通常不应该是空的。所以如果它是空的,在你的代码中有一个bug,你需要追踪。但是上面的代码永远不会告诉你这一点。因此,在编写“安全”代码的善意努力中,您也隐藏了一个错误。这样写代码会更好:

/**
 * @param Widget widget Should never be null
 */
void doSomething(Widget widget) {
  assert widget != null;
  widget.someMethod(); // ...
    ... // do more stuff with this widget
}

这样,您一定能尽早发现这个错误。(在合同中指定这个参数永远不应该为空也是有用的。)在开发过程中测试代码时,一定要打开断言。(说服你的同事这样做通常也很困难,我觉得这很烦人。)

现在,您的一些同事会反对这段代码,认为您仍然应该放入null检查,以防止生产中出现异常。在这种情况下,断言仍然有用。你可以这样写:

void doSomething(Widget widget) {
  assert widget != null;
  if (widget != null) {
    widget.someMethod(); // ...
    ... // do more stuff with this widget
  }
}

这样,您的同事就会高兴地看到产品代码有空检查,但在开发过程中,当小部件为空时,您就不再隐藏错误了。

这里有一个真实的例子:我曾经写过一个方法,比较两个任意值是否相等,其中任何一个值都可以为空:

/**
 * Compare two values using equals(), after checking for null.
 * @param thisValue (may be null)
 * @param otherValue (may be null)
 * @return True if they are both null or if equals() returns true
 */
public static boolean compare(final Object thisValue, final Object otherValue) {
  boolean result;
  if (thisValue == null) {
    result = otherValue == null;
  } else {
    result = thisValue.equals(otherValue);
  }
  return result;
}

这段代码在thisValue不为空的情况下委托equals()方法的工作。但它假设equals()方法通过正确处理空参数正确地实现了equals()的契约。

一位同事反对我的代码,告诉我我们的许多类都有不测试null的equals()方法,所以我应该把这个检查放到这个方法中。这是否是明智的,或者我们是否应该强制错误,这样我们就可以发现并修复它,这是有争议的,但我听从了我同事的意见,放入了一个空检查,我已经标记了一个注释:

public static boolean compare(final Object thisValue, final Object otherValue) {
  boolean result;
  if (thisValue == null) {
    result = otherValue == null;
  } else {
    result = otherValue != null && thisValue.equals(otherValue); // questionable null check
  }
  return result;
}

这里的额外检查other != null仅在equals()方法不能按照其契约要求检查null时才有必要。

我没有与同事就让有bug的代码留在代码库中是否明智进行毫无结果的争论,而是简单地在代码中放入了两个断言。这些断言会让我知道,在开发阶段,如果我们的一个类不能正确地实现equals(),所以我可以修复它:

public static boolean compare(final Object thisValue, final Object otherValue) {
  boolean result;
  if (thisValue == null) {
    result = otherValue == null;
    assert otherValue == null || otherValue.equals(null) == false;
  } else {
    result = otherValue != null && thisValue.equals(otherValue);
    assert thisValue.equals(null) == false;
  }
  return result;
}

需要记住的要点如下:

Assertions are development-phase tools only. The point of an assertion is to let you know if there's a bug, not just in your code, but in your code base. (The assertions here will actually flag bugs in other classes.) Even if my colleague was confident that our classes were properly written, the assertions here would still be useful. New classes will be added that might fail to test for null, and this method can flag those bugs for us. In development, you should always turn assertions on, even if the code you've written doesn't use assertions. My IDE is set to always do this by default for any new executable. The assertions don't change the behavior of the code in production, so my colleague is happy that the null check is there, and that this method will execute properly even if the equals() method is buggy. I'm happy because I will catch any buggy equals() method in development.

此外,您应该通过放入一个将失败的临时断言来测试断言策略,这样您就可以确定通过日志文件或输出流中的堆栈跟踪来通知您。

什么时候应该使用Assert ?

很多很好的答案解释了assert关键字的作用,但很少回答真正的问题,“在现实生活中什么时候应该使用assert关键字?”答案是:

几乎从来没有

断言,作为一个概念,是很棒的。好的代码有很多if(…)throw…语句(以及它们的亲戚,如对象。requireNonNull和Math.addExact)。然而,某些设计决策极大地限制了assert关键字本身的效用。

assert关键字背后的驱动思想是不成熟的优化,其主要特性是能够轻松地关闭所有检查。事实上,断言检查在默认情况下是关闭的。

然而,在生产中继续执行不变检查是非常重要的。这是因为完美的测试覆盖率是不可能的,所有的产品代码都会有错误,而断言应该有助于诊断和减轻这些错误。

因此,if(…)的使用抛出…应该是首选的,就像检查公共方法的参数值和抛出IllegalArgumentException时需要它一样。

偶尔,人们可能会被诱惑编写一个不变检查,它确实需要很长时间来处理(并且经常被调用,以至于它很重要)。然而,这样的检查将减缓测试,这也是不可取的。这种耗时的检查通常被写成单元测试。然而,出于这个原因,有时使用assert是有意义的。

不要使用assert,因为它比if(…)throw…(我非常痛苦地说,因为我喜欢干净和漂亮)。如果您无法控制自己,并且可以控制应用程序的启动方式,那么可以随意使用断言,但始终在生产环境中启用断言。不可否认,这是我倾向于做的事情。我正在推动一个lombok注释,它将导致assert的行为更像if(…)throw ....请在这里投票。

(咆哮:JVM开发者是一群糟糕的、过早优化的编码员。这就是为什么你会在Java插件和JVM中听到这么多安全问题。他们拒绝在产品代码中包含基本的检查和断言,而我们正在继续为此付出代价。)

总结一下(不仅仅是Java,很多语言都是如此):

“assert”主要被软件开发人员在调试过程中用作调试辅助。断言消息永远不会出现。许多语言提供了一个编译时选项,该选项将导致所有“断言”被忽略,用于生成“生产”代码。

"exceptions" are a handy way to handle all kinds of error conditions, whether or not they represent logic errors, because, if you run into an error-condition such that you cannot continue, you can simply "throw them up into the air," from wherever you are, expecting someone else out there to be ready to "catch" them. Control is transferred in one step, straight from the code that threw the exception, straight to the catcher's mitt. (And the catcher can see the complete backtrace of calls that had taken place.)

此外,该子例程的调用者不必检查子例程是否成功:“如果我们现在在这里,它一定成功了,因为否则它会抛出异常,我们现在就不会在这里!”这种简单的策略使得代码设计和调试变得非常非常容易。

异常方便地允许致命错误条件保持原样:“规则的异常”。而且,对于它们来说,由代码路径来处理,这也是“规则的例外……“高飞球!”

这是另一个例子。我写了一个方法来查找两个排序数组中值的中位数。该方法假设数组已经排序。出于性能考虑,它不应该首先对数组排序,甚至不应该检查以确保它们已排序。然而,对未排序的数据调用此方法是一个严重的错误,我们希望在开发阶段尽早发现这些错误。下面是我处理这些看似矛盾的目标的方法:

public static int medianOf(int[] a, int[] b) {
  assert assertionOnlyIsSorted(a);      // Assertion is order n
  assert assertionOnlyIsSorted(b);
  ... // rest of implementation goes here. Algorithm is order log(n)
}

public static boolean assertionOnlyIsSorted(int[] array) {
  for (int i=1; i<array.length; ++i) {
    if (array[i] < array[i-1]) {
      return false;
    }
    return true;
  }
}

这样,缓慢的测试只在开发阶段执行,在开发阶段,速度没有捕获错误重要。您希望medianOf()方法具有log(n)性能,但“is sorted”测试是o (n)。因此,我将其放在断言中,以限制其在开发阶段的使用,并为其命名,以明确表示它不适合生产。

这样我就两全其美了。在开发过程中,我知道任何不正确地调用这个函数的方法都会被捕获并修复。而且我知道这样做的缓慢测试不会影响生产中的性能。(这也很好地说明了为什么要在生产环境中关闭断言,而在开发环境中启用断言。)

Assert是一个关键字。它在JDK 1.4中引入。这是两种类型的断言

非常简单的断言语句 简单的断言语句。

默认情况下,所有assert语句都不会被执行。如果assert语句接收到false,则它将自动引发断言错误。