一尘不染

多少个null检查就足够了?

java

什么是当它的一些准则 不是 必要的检查空?

到目前为止,我一直在处理的许多继承代码都带有null检查广告。对琐碎的函数进行空检查,对声明非空返回的API调用进行空检查,等等。在某些情况下,空检查是合理的,但在许多地方,空值不是合理的期望。

我听到过许多参数,从“您不能信任其他代码”到“始终防御性地编程”再到“直到语言保证我没有非空值,我总是要检查”。在某种程度上,我当然同意其中的许多原则,但是我发现过多的null检查会导致其他通常违反这些原则的问题。顽强的null检查真的值得吗?

通常,我观察到带有过多空值检查的代码实际上质量较差,而不是质量较高。许多代码似乎都集中在null检查上,以至于开发人员已经忽略了其他重要特性,例如可读性,正确性或异常处理。特别是,我看到很多代码都忽略了std
:: bad_alloc异常,但是对进行了空检查new

在C
++中,由于解除引用空指针的行为具有不可预测的特性,因此我在某种程度上理解了这一点。在Java,C#,Python等中,可以更轻松地处理null取消引用。我刚刚看过一些不良示例(例如,警惕的null检查)还是真的有什么用?

尽管我主要对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.
}

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

如果您可以查看并可以更新的方法(或者可以大喊大叫的开发人员)已签有合同,那么是否仍然需要检查null?

//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;
}

阅读 268

收藏
2020-12-03

共1个答案

一尘不染

首先请注意,这是合同检查的一种特殊情况:您编写的代码除了在运行时验证是否满足书面合同外,别无其他。失败意味着某处的某些代码有错误。

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

关于输入验证的主题:

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

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

那么问题就变成了,NULL是足够普遍的无效输入,可以得到(foo *)(-1)没有得到的特殊待遇吗?

与Java不同,字段不会自动初始化为NULL,因此未初始化的垃圾值与NULL一样合理。但是有时候C
++对象的指针成员明确地用NULL初始化,这意味着“我还没有”。如果您的调用者这样做,则存在大量的编程错误,可以通过NULL检查来诊断。与他们没有源的库中的页面错误相比,对于他们而言,调试异常可能更容易。因此,如果您不介意代码膨胀,可能会有所帮助。但这不是您自己,而是您应该考虑的呼叫者-
这不是防御性的编码,因为它仅针对NULL而不针对(foo *)(-1)来“防御”。

如果NULL不是有效的输入,则可以考虑通过引用而不是指针来获取参数,但是许多编码方式均反对使用非const引用参数。并且,如果调用者通过了*
fooptr(其中fooptr为NULL),那么无论如何它都无济于事。您想要做的是将更多文档压缩到函数签名中,希望您的调用者更有可能认为“嗯,在这里fooptr是否为null?”。当他们不得不显式地取消引用它时,就好比将它们作为指针传递给您一样。它只是走了很远,但就它可能会有所帮助。

我不了解C#,但我了解这就像Java,保证引用具有有效值(至少在安全代码中),但是与Java不同,并非所有类型都具有NULL值。因此,我猜想null检查几乎没有什么价值:如果您使用的是安全代码,则不要使用可为空的类型,除非null是有效的输入;如果您使用的是不安全的代码,则适用与在C
++中。

关于输出验证的主题:

出现类似的问题:在Java中,您可以通过了解输出的类型来“完全验证”输出,并且该值不为null。在C ++中,您无法使用NULL检查“完全验证”输出-
因为您所知道的所有函数都返回了指向其自身已被取消缠绕的对象的指针。但是,如果由于被调用者代码的创建者通常使用的结构而导致NULL是常见的无效返回,则进行检查将有所帮助。

在所有情况下:

尽可能使用断言而不是“真实代码”来检查合同-应用程序正常工作后,您可能不希望每个被调用方检查其所有输入,而每个调用方都检查其返回值的代码膨胀。

在编写可移植到非标准C ++实现的代码的情况下,我可能会拥有一个类似于以下功能的函数:而不是问题中检查空值并捕获异常的代码:

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
++实现上对其进行编译,则出于根本原因,他们基本上是出于自己的意愿,所以他们必须自己做。

2020-12-03