多少空值检查就足够了?

什么是什么是什么时候没有必要检查空值的指导原则?

最近我一直在研究的很多继承代码都有空的检查和恶心。 对平凡函数进行空检查,对调用非空返回的API调用进行空检查等。在某些情况下,空检查是合理的,但在很多地方,空值不是合理的期望值。

我听到过许多争论,从“你不能相信其他代码”到“始终防守”,直到“在语言保证我是非空值的前提下,我总是会去检查。” 我当然同意这些原则中的许多原则,但我发现过多的空值检查会导致通常违反这些原则的其他问题。 顽强的空检查真的值得吗?

通常情况下,我观​​察到过量空值检查的代码实际上质量较差,而质量较差。 大部分代码似乎都非常关注空值检查,以至于开发人员忽略了其他重要特性,例如可读性,正确性或异常处理。 特别是,我看到很多代码忽略了std :: bad_alloc异常,但是对new进行了空检查。

在C ++中,由于取消引用空指针的不可预知的行为,我在某种程度上理解了这一点; 在Java,C#,Python等中,空取消引用的处理更加优雅。我刚才看到了警惕空检查的不好的例子,还是真的有这种情况?

这个问题旨在成为语言不可知论者,尽管我主要对C ++,Java和C#感兴趣。


我已经看到过一些似乎过度的空检查的例子,包括以下内容:


这个例子似乎是非标准编译器的原因,因为C ++规范说新失败的抛出异常。 除非您明确支持不符合规范的编译器,否则这是否有意义? 这对Java或C#(甚至是C ++ / CLR)之类的托管语言有什么意义吗?

try {
   MyObject* obj = new MyObject(); 
   if(obj!=NULL) {
      //do something
   } else {
      //??? most code I see has log-it and move on
      //or it repeats what's in the exception handler
   }
} catch(std::bad_alloc) {
   //Do something? normally--this code is wrong as it allocates
   //more memory and will likely fail, such as writing to a log file.
}

另一个例子是在处理内部代码时。 特别是,如果它是一个能够定义自己开发实践的小团队,这似乎没有必要。 在某些项目或遗留代码中,信任文档可能不合理......但对于您或您的团队控制的新代码,这是否真的有必要?

如果您可以看到并且可以更新(或者可以对负责的开发人员大吼)的方法拥有合同,是否仍然有必要检查空值?

//X is non-negative.
//Returns an object or throws exception.
MyObject* create(int x) {
   if(x<0) throw;
   return new MyObject();
}

try {
   MyObject* x = create(unknownVar);
   if(x!=null) {
      //is this null check really necessary?
   }
} catch {
   //do something
}

在开发私有函数或其他内部函数时,是否真的有必要在合约仅调用非空值时显式处理空值? 为什么空检查比断言更可取?

(显然,在您的公共API上,空值检查是至关重要的,因为认为用户不正当地使用该API是不礼貌的)

//Internal use only--non-public, not part of public API
//input must be non-null.
//returns non-negative value, or -1 if failed
int ParseType(String input) {
   if(input==null) return -1;
   //do something magic
   return value;
}

相比:

//Internal use only--non-public, not part of public API
//input must be non-null.
//returns non-negative value
int ParseType(String input) {
   assert(input!=null : "Input must be non-null.");
   //do something magic
   return value;
}

首先需要注意的是,这是一个合同检查的特例:您正在编写的代码除了在运行时验证文档合同得到满足之外,什么也不做。 失败意味着某处的某些代码有问题。

对于实施更普遍有用的概念的特殊情况,我总是有点怀疑。 契约检查很有用,因为它在第一次跨越API边界时捕获编程错误。 关于空值有什么特别之处,意味着它们是您关心检查的合约的唯一部分? 仍然,

关于输入验证的主题:

null在Java中是特殊的:编写了很多Java API,使得null是唯一无效的值,它甚至可以传递给给定的方法调用。 在这种情况下,空检查“完全验证”输入,因此支持合同检查的完整论点适用。

另一方面,在C ++中,由于几乎所有的地址都不是正确类型的对象,所以NULL只是指针参数可能采用的无效值的近2 ^ 32(在新体系结构上为2 ^ 64)之一。 除非在该类型的所有对象中都有一个列表,否则不能“完全验证”您的输入。

现在的问题是,NULL是否是一个足够常见的无效输入以获得(foo *)(-1)没有得到的特殊处理?

与Java不同,字段不会自动初始化为NULL,所以垃圾未初始化的值与NULL一样合理。 但有时C ++对象的指针成员显式为NULL-inited,意思是“我还没有一个”。 如果你的调用者这样做,那么有一个重要的程序错误类可以通过NULL检查来诊断。 一个异常可能比他们没有源代码的库中的页面错误更容易调试。 所以如果你不介意代码膨胀,它可能会有所帮助。 但是你应该考虑的是你的调用者,而不是你自己 - 这不是防御性编码,因为它只是针对NULL进行“防御”,而不是针对(foo *)( - 1)。

如果NULL不是有效的输入,你可以考虑通过引用而不是指针来获取参数,但是很多编码风格不赞成非const引用参数。 如果调用者通过你* fooptr,其中fooptr为NULL,那么它无论如何都没有任何好处。 你想要做的是在函数签名中加入更多的文档,希望你的调用者更可能会认为“嗯,可能在这里是空的?” 当他们必须明确地解除引用时,比将它作为指针传递给你。 它只能走得这么远,但就目前而言,它可能会有所帮助。

我不知道C#,但我明白这就像Java一样,引用保证有有效值(至少在安全代码中),但与Java不同的是,并不是所有类型都有NULL值。 所以我猜测那些空检查是很不值得的:如果你在安全的代码中,那么不要使用可为空的类型,除非null是一个有效的输入,并且如果你在不安全的代码中,那么相同的推理适用于在C ++中。

关于输出验证的主题:

出现类似的问题:在Java中,您可以通过了解它的类型来“完全验证”输出,并且该值不为空。 在C ++中,你不能用NULL检查“完全验证”输出 - 对于你所知道的所有函数都会返回一个指向它自己的栈上的对象的指针,它刚刚被解开。 但是,如果由于被调用者代码的作者通常使用的构造,NULL是常见的无效返回,那么检查它将会有所帮助。

在所有情况下:

使用断言而不是“真实代码”在可能的情况下检查合约 - 一旦您的应用程序正在工作,您可能不希望每个被调用者的代码膨胀检查其所有输入,并且每个调用者检查其返回值。

在编写可移植到非标准C ++实现的代码的情况下,代替检查null并捕获异常的问题中的代码,我可能会有这样的函数:

template<typename T>
static inline void nullcheck(T *ptr) { 
    #if PLATFORM_TRAITS_NEW_RETURNS_NULL
        if (ptr == NULL) throw std::bad_alloc();
    #endif
}

然后,作为移植到新系统时所做的事情之一,您可以正确定义PLATFORM_TRAITS_NEW_RETURNS_NULL(也可能包含其他一些PLATFORM_TRAITS)。 很明显,你可以写一个头文件来处理你所知道的所有编译器。 如果有人需要你的代码并将它编译到一个你不知道的非标准C ++实现上,那么他们基本上是靠自己的原因而不是自己的,所以他们必须自己做。


有一件事要记住,你今天编写的代码虽然可能是一个小团队,并且你可以拥有良好的文档,但是会变成其他人不得不维护的遗留代码。 我使用以下规则:

  • 如果我正在编写一个公开的API,它将暴露给其他人,那么我将对所有参考参数进行空检查。

  • 如果我正在向应用程序写入内部组件,那么当我需要在存在空值时或者想要使其非常明确时需要做一些特殊的操作时,我会写空检查。 否则,我不介意得到空引用异常,因为这也很清楚发生了什么。

  • 在处理来自其他人框架的返回数据时,只有在返回空值的情况下才能检查null。 如果他们的合同说它不返回空值,我不会执行检查。


  • 如果您编写代码和合同,您有责任根据合同使用它并确保合同是正确的。 如果你说“返回一个非null”x,那么调用者不应该检查null。 如果那个引用/指针出现空指针异常,那么你的合约是不正确的。

    只有在使用不可信的库或没有适当的合同时,空检查才会变得极端。 如果这是您的开发团队的代码,则强调不得中断合同,并在发生错误时正确追踪使用合同的人员。

    链接地址: http://www.djcxy.com/p/21429.html

    上一篇: How much null checking is enough?

    下一篇: Hashtable in C++?