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) 检查!是不是太多了?就算像上面一样优化掉了,间接调用或者长函数这种情况,估计不会被内联怎么办?
那么,我的程序应该按照下面的规则来写吗?
- 如果一个 API 函数可能不会被内联或预计会被调用很多次,并在调用站点上进行检查(参见
vector
示例),assert()
它的先决条件(在里面);
- try-catch 抛出函数而不是在调用前检查它们的先决条件(后者似乎打破了 DRY)。
如果不是,为什么?
所以,您在谈论两个不同的事情: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
的函数,不需要检查是否满足前置条件,因为它只是被创建如果有的话。
很多 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) 检查!是不是太多了?就算像上面一样优化掉了,间接调用或者长函数这种情况,估计不会被内联怎么办?
那么,我的程序应该按照下面的规则来写吗?
- 如果一个 API 函数可能不会被内联或预计会被调用很多次,并在调用站点上进行检查(参见
vector
示例),assert()
它的先决条件(在里面); - try-catch 抛出函数而不是在调用前检查它们的先决条件(后者似乎打破了 DRY)。
如果不是,为什么?
所以,您在谈论两个不同的事情: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
的函数,不需要检查是否满足前置条件,因为它只是被创建如果有的话。