if-throw 前置条件检查有效性和 DRY 原则

if-throw precondition check effectiveness and the DRY principle

很多 Internet 资源坚持通过 if (something_is_wrong) throw Exception{} 而不是 assert(!something_is_wrong) 检查 API 函数中的前提条件,我看到了其中的一些优点。但是,恐怕这样的用法可能会导致相同的检查加倍:

void foo(int positive) {
  if (positive < 1) {
    throw std::invalid_argument("param1 must be positive");
  }
}
void caller() {
  int number = get_number_somehow();
  if (number >= 1) {
    foo(number);
  }
}

可能会像

那样执行
int number = get_number_somehow();
if (number >= 1) {
  if (number < 1) {
    throw std::invalid_argument("param must be positive");
  }
}

除非通过删除其中一个 if 来实际内联和优化调用,我猜。此外,两次写检查(在 foo()caller() 中)可能违反了 DRY 规则。因此,也许我应该去

void caller() {
  int number = get_number_somehow();
  try {
    foo(number);
  } catch (std::invalid_argument const&) {
    // handle or whatever
  }
}

为了避免重复这些前提条件检查,在函数契约更改的情况下提供一点性能和大量可维护性。

但是,我不能总是应用这样的逻辑。想象一下 std::vector 只有 at() 但没有 operator[]:

for (int i = 0; i < vector.size(); ++i) {
  bar(vector.at(i)); // each index is checked twice: by the loop and by at()
}

此代码导致额外的 O(N) 检查!是不是太多了?就算像上面一样优化掉了,间接调用或者长函数这种情况,估计不会被内联怎么办?

那么,我的程序应该按照下面的规则来写吗?

如果不是,为什么?

所以,您在谈论两个不同的事情:DRY 和性能。

DRY 是关于代码维护和结构的,并不真正适用于您无法控制的代码。所以,如果 API 是一个黑盒子,并且里面恰好有你无法更改的代码,但你需要单独拥有,那么我不会认为它不是 DRY to在您的代码中重复它。 Y是你自己。

但是,您仍然可以关心性能。如果你衡量一个性能问题,然后用任何有意义的方式修复它——即使它是反 DRY 的(或者如果是的话也没关系)。

但是,如果您控制双方(API 和客户端)并且您真的想要一个纯粹的、无重复的、高性能的解决方案,那么就会有一个类似于这个伪代码的模式。我不知道名字,但我认为它是 "Proof Providing"

let fn = precondition_check(myNum)
if fn != nil {
    // the existence of fn is proof that myNum meets preconditions
    fn()
}

API func precondition_check returns 捕获其中myNum的函数,不需要检查是否满足前置条件,因为它只是被创建如果有的话。