if (this.discountRate)
  base = base - (this.discountRate * base);
 
 
  assert(this.discountRate>= 0);
if (this.discountRate)
  base = base - (this.discountRate * base);

动机

常常会有这样一段代码:只有当某个条件为真时,该段代码才能正常运行。例如,平方根计算只对正值才能进行,又例如,某个对象可能假设一组字段中至少有一个不等于 null。

这样的假设通常并没有在代码中明确表现出来,你必须阅读整个算法才能看出。有时程序员会以注释写出这样的假设,而我要介绍的是一种更好的技术——使用断言明确标明这些假设。

断言是一个条件表达式,应该总是为真。如果它失败,表示程序员犯了错误。断言的失败不应该被系统任何地方捕捉。整个程序的行为在有没有断言出现的时候都应该完全一样。实际上,有些编程语言中的断言可以在编译期用一个开关完全禁用掉。

我常看见有人鼓励用断言来发现程序中的错误。这固然是一件好事,但却不是使用断言的唯一理由。断言是一种很有价值的交流形式——它们告诉阅读者,程序在执行到这一点时,对当前状态做了何种假设。另外断言对调试也很有帮助。而且,因为它们在交流上很有价值,即使解决了当下正在追踪的错误,我还是倾向于把断言留着。自测试的代码降低了断言在调试方面的价值,因为逐步逼近的单元测试通常能更好地帮助调试,但我仍然看重断言在交流方面的价值。

做法

如果你发现代码假设某个条件始终为真,就加入一个断言明确说明这种情况。

因为断言应该不会对系统运行造成任何影响,所以“加入断言”永远都应该是行为保持的。

范例

下面是一个简单的例子:折扣。顾客(customer)会获得一个折扣率(discount rate),可以用于所有其购买的商品。

class Customer…

applyDiscount(aNumber) {
  return (this.discountRate)
    ? aNumber - (this.discountRate * aNumber)
    : aNumber;
}

这里有一个假设:折扣率永远是正数。我可以用断言明确标示出这个假设。但在一个三元表达式中没办法很简单地插入断言,所以我首先要把这个表达式转换成 if-else 的形式。

class Customer…

applyDiscount(aNumber) {
  if (!this.discountRate) return aNumber;
  else return aNumber - (this.discountRate * aNumber);
}

现在我就可以轻松地加入断言了。

class Customer…

applyDiscount(aNumber) {
  if (!this.discountRate) return aNumber;
  else {
    assert(this.discountRate >= 0);
    return aNumber - (this.discountRate * aNumber);
  }
}

对这个例子而言,我更愿意把断言放在设值函数上。如果在 applyDiscount 函数处发生断言失败,我还得先费力搞清楚非法的折扣率值起初是从哪儿放进去的。

class Customer…

set discountRate(aNumber) {
  assert(null === aNumber || aNumber >= 0);
  this._discountRate = aNumber;
}

真正引起错误的源头有可能很难发现——也许是输入数据中误写了一个减号,也许是某处代码做数据转换时犯了错误。像这样的断言对于发现错误源头特别有帮助。

注意,不要滥用断言。我不会使用断言来检查所有“我认为应该为真”的条件,只用来检查“必须为真”的条件。滥用断言可能会造成代码重复,尤其是在处理上面这样的条件逻辑时。所以我发现,很有必要去掉条件逻辑中的重复,通常可以借助提炼函数(106)手法。

我只用断言预防程序员的错误。如果要从某个外部数据源读取数据,那么所有对输入值的检查都应该是程序的一等公民,而不能用断言实现——除非我对这个外部数据源有绝对的信心。断言是帮助我们跟踪 bug 的最后一招,所以,或许听来讽刺,只有当我认为断言绝对不会失败的时候,我才会使用断言。