我应该检查一个函数的每个参数以确保该函数运行良好吗?
我注意到,标准C库包含几个字符串函数不检查输入参数(无论是NULL),喜欢的strcmp:我应该检查一个函数的每个参数以确保该函数运行良好吗?
int strcmp(const char *s1, const char *s2)
{
for (; *s1 == *s2; s1++, s2++)
if (*s1 == '\0')
return 0;
return ((*(unsigned char *)s1 < *(unsigned char *)s2) ? -1 : +1);
}
和很多人不这样做相同的验证。这是一个很好的做法吗?
在其他图书馆,我看到他们检查每一个参数,像这样:
int create_something(int interval, int mode, func_t cb, void *arg, int id)
{
if (interval == 0) return err_code_1;
if (valid(mode)) return err_code_2;
if (cb == NULL) return err_code_3;
if (arg == NULL) return err_code_4;
if (id == 0) return err_code_5;
// ...
}
哪一个更好?当你设计一个API时,你会检查所有的参数以使它正常工作,或者让它崩溃吗?
我想认为,在库函数不检查指针NULL
期望有效的指针实际上比做错误返回或默默地忽略它们更好。
NULL
不是唯一的无效指针。还有数十亿其他指针值实际上是不正确的,为什么我们只给予一个值的优惠待遇呢?
错误返回往往被忽略,误解或管理不善。忘记检查一个错误返回可能导致一个行为不当的程序。我想争辩说,一个默默无闻的程序比一个根本不起作用的程序更糟糕。不正确的结果可能比没有结果更糟。
失败及早,难以调试。这是最大的原因。程序的最终用户不希望程序崩溃,但作为程序员,我是库的最终用户,我真的希望它崩溃。崩溃使得很明显,我需要修复一个错误,而且我们碰到错误的速度越快,崩溃越接近错误来源,越快越容易找到并修复错误。 A NULL
指针解引用是捕获,调试和修复最重要的错误之一。这比通过千兆字节的日志拖网来发现一条显示“create_something有空指针”的行更容易。
如果错误返回,如果调用者捕获该错误,返回一个错误本身(在您的示例中将是err_create_something_failed
),并且其调用者返回另一个错误(err_caller_of_create_something_failed
)?然后你有一个错误返回3个函数,甚至可能不能指出实际发生了什么错误。即使它能够指出实际发生了什么错误(通过整个错误处理框架记录整个调用者链中发生错误的位置),您可以使用的唯一方法是查找错误值一些表,并从中得出结论create_something
中有一个NULL
指针。这是一个很大的痛苦,相反,你可能只是打开了一个调试器,并且确切地看到了这个假设被违反的位置,以及确切的函数调用链导致了这个问题。
以同样的精神,您可以使用assert
来验证其他函数参数,以便尽早且轻松地调试故障。断言断言,你有完全正确的调用链,导致问题。我只是不会使用断言来检查指针,因为它没有意义(至少在带有内存管理的操作系统上),并且在给予相同行为(减去打印的消息)的同时使事情变得更慢。
您可以使用ASSERT.H检查您的参数:
assert(pointer != NULL);
这将使该程序无法在调试模式下,如果“ponter == NULL”,但不会有任何检查都在发布这样你可以检查你想要的一切,而不会影响性能。
无论如何,如果函数需要范围内的参数检查是浪费资源,那么API的用户应该执行检查。
但是由您决定如何设计API。在这个问题上没有正确的方法:如果函数需要1到5之间的数字,并且用户传递了6,则可以执行检查或简单地指定函数将具有未定义的行为。
没有通用的方法来执行参数验证。通常,在可以验证参数时应该使用assert
,但通常在非调试版本中禁用assert
,并且可能并不总是合适的。
有几件事情要考虑,可以从情况而异,如:
- 你希望你的函数被称为很多?性能至关重要?如果一个调用者会在一个紧密的循环中多次调用你的函数,那么验证参数会很昂贵。对于内联函数,如果验证检查的运行时成本超出了函数的其余部分的运行时间成本,这对于内联函数尤其不利。
- 调用者是否容易执行检查?如果检查是不平凡的,那么在函数本身中进行验证比在调用者上强加额外工作更不容易出错。请注意,在某些情况下,调用者甚至可能无法自行执行适当的验证(例如,如果在检查参数的有效性时存在争用条件的可能性)。
- 您的功能是否有充分记录?它是否清楚地描述了它的先决条件,指定其参数的有效值是什么?如果是这样,那么你通常应该认为它是传递有效参数的调用者的责任。
- 您的功能是否自行记录?调用者显而易见的是什么有效的参数?
- 应该传递一个错误的参数成为逻辑错误或运行时错误?也就是说,如果它被认为是程序员的错误?争论可能直接来自用户输入?你应该考虑你期望呼叫者如何使用你的功能。如果断言被启用,应该是一个坏论点致命并终止程序?
- 谁是你的功能用户?您的函数是否会在内部使用(您可能对其他程序员使用它的能力有所期待),还是会暴露给公众?如果是后者,哪种故障模式会最大限度地减少您需要提供的技术支持的数量? (我把我的dropt library的立场是,我靠断言验证参数内部功能和报告的错误码公共职能。)
我注意到,标准C库包含几个字符串函数不检查输入参数(无论是NULL),喜欢的strcmp:
标准C库的
字符串处理函数要求“......在这样的调用上的指针参数仍应具有有效值......”C11dr§7.24。1 2
NULL
不是指向字符串的有效指针,并且函数的部分没有检查指针有效性的要求,所以没有NULL
检查。 C的表现的确是有代价的。
当你设计一个API时,你会检查所有的参数,使它运行良好或只是让它崩溃?
这种进入设计理念。考虑一个更简单的例子。 API是否应该首先测试输入参数?这取决于编码目标。
int add(int a, int b) {
return a + b;
}
// return 1 on failure
int add_safe(int *sum, int a, int b) {
if (a >= 0) {
if (b > INT_MAX - a) return 1; // Overflow
} else {
if (b < INT_MIN - a) return 1; // Underflow
} if (sum) { // NULL check
*sum = a + b;
}
return 0;
}
有疑问时,创建一个具有名义检查的API。使用强烈检查可能来自用户(人类)或其他进程的输入。如果你想要严重的迂腐检查,C不是一个有效的目标语言选择。
随着许多我已作出API,NULL
是一个有效输入值作为一个指针和代码调整其功能基础上,由于上方。一个NULL
检查制成,但它不是一个错误检查。
更好地提供强有力的类型,因此没有必要的检查。然后检查是更好的,但它不是免费的。所以出于性能原因,在几个接口中省略了检查(但是它应该在文档中)。 – Jarod42
[这次谈话(https://www.youtube.com/watch?v=1QhtXRMp3Hg)进入非常详细如何做到这一点,为什么。总之,您必须了解后果(绩效,合同变更),做出决定,记录并保持一致。 – nwp
只要您记录约束条件并说出_undefined behavior_,否则它可能是性能原因的合理选择。 – skypjack