什么是什么时候没有必要检查空?
我最近一直在研究的许多继承代码都有空检查令人作呕.Null检查普通函数,对表示非空返回的API调用进行空检查等.在某些情况下,空检查是合理的,但在许多地方,null不是合理的期望.
我听过很多论点,从"你不能相信其他代码"到"总是防守程序"到"直到语言保证我的非空值,我总会检查." 我当然同意这些原则中的许多原则,但我发现过多的空值检查会导致其他通常违反这些原则的问题.顽强的空检查真的值得吗?
通常,我观察到过多的空检查代码实际上质量较差,而不是质量较高.许多代码似乎都专注于空检查,开发人员已经忽略了其他重要的特性,例如可读性,正确性或异常处理.特别是,我看到很多代码忽略了std :: bad_alloc异常,但对a进行了空检查new
.
在C++中,由于解除引用空指针的不可预测行为,我在某种程度上理解这一点; 在Java,C#,Python等中更优雅地处理null dereference.我刚刚看到了警惕的空检查的不良例子,还是真的有什么东西可以解决这个问题?
这个问题旨在与语言无关,尽管我主要对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 }
在开发私有函数或其他内部函数时,当合约只调用非空值时,是否真的有必要显式处理null?为什么null检查比断言更可取?
(显然,在您的公共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; }
JoshBerke.. 36
有一点要记住,您今天编写的代码虽然可能是一个小团队而且您可以拥有良好的文档,但会变成其他人必须维护的遗留代码.我使用以下规则:
如果我正在编写一个将向其他人公开的公共API,那么我将对所有引用参数进行空检查.
如果我正在为我的应用程序编写一个内部组件,当我需要在null存在时执行某些特殊操作时,或者当我想要非常清楚时,我会编写空检查.否则我不介意获取空引用异常,因为这也很清楚发生了什么.
当处理来自其他人框架的返回数据时,我只检查null是否可能并且返回null是有效的.如果他们的合同说它没有返回空值,我将不会进行检查.
Steve Jessop.. 19
首先请注意,这是一个特殊的合同检查案例:您编写的代码除了在运行时验证是否符合文档化合同之外什么都不做.失败意味着某些代码在某处出错.
对于实现更普遍有用的概念的特殊情况,我总是略显怀疑.合同检查很有用,因为它在第一次跨越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中,您可以通过了解其类型来"完全验证"输出,并且该值不为null.在C++中,您无法通过NULL检查"完全验证"输出 - 因为您知道该函数返回了指向其自身堆栈上刚刚解开的对象的指针.但是如果由于被调用者代码的作者通常使用的构造,NULL是一个常见的无效返回,那么检查它将有所帮助.
在所有情况下:
使用断言而不是"真实代码"来尽可能检查合同 - 一旦你的应用程序工作,你可能不希望每个被调用者的代码膨胀检查其所有输入,并且每个调用者检查其返回值.
在编写可以移植到非标准C++实现的代码的情况下,那么代替检查null并且还捕获异常的问题中的代码,我可能有这样的函数:
templatestatic 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是否可能并且返回null是有效的.如果他们的合同说它没有返回空值,我将不会进行检查.
首先请注意,这是一个特殊的合同检查案例:您编写的代码除了在运行时验证是否符合文档化合同之外什么都不做.失败意味着某些代码在某处出错.
对于实现更普遍有用的概念的特殊情况,我总是略显怀疑.合同检查很有用,因为它在第一次跨越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中,您可以通过了解其类型来"完全验证"输出,并且该值不为null.在C++中,您无法通过NULL检查"完全验证"输出 - 因为您知道该函数返回了指向其自身堆栈上刚刚解开的对象的指针.但是如果由于被调用者代码的作者通常使用的构造,NULL是一个常见的无效返回,那么检查它将有所帮助.
在所有情况下:
使用断言而不是"真实代码"来尽可能检查合同 - 一旦你的应用程序工作,你可能不希望每个被调用者的代码膨胀检查其所有输入,并且每个调用者检查其返回值.
在编写可以移植到非标准C++实现的代码的情况下,那么代替检查null并且还捕获异常的问题中的代码,我可能有这样的函数:
templatestatic 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++实现上,那么他们基本上是出于更大的原因而不是自己,所以他们必须自己做.
如果您编写代码及其合同,您有责任在合同中使用它并确保合同正确.如果您说"返回非空"x,则调用者不应检查null.如果使用该引用/指针发生空指针异常,则表示您的合同不正确.
当使用不受信任的库或没有合适的合同时,空检查应该只是极端.如果是您的开发团队的代码,请强调不得破坏合同,并在发生错误时错误地跟踪使用合同的人员.
部分原因取决于代码的使用方式 - 例如,它是仅在项目中与公共API相关的方法.API错误检查需要比断言更强大的功能.
因此,虽然这在一个项目中很好,但它支持单元测试和类似的东西:
internal void DoThis(Something thing) { Debug.Assert(thing != null, "Arg [thing] cannot be null."); //... }
在一种方法中,你无法控制谁调用它,这样的事情可能会更好:
public void DoThis(Something thing) { if (thing == null) { throw new ArgumentException("Arg [thing] cannot be null."); } //... }
这取决于实际情况.我的其余部分假设是C++.
我从不测试new的返回值,因为我使用的所有实现都会在失败时抛出bad_alloc.如果我在我正在处理的任何代码中看到新的返回null的遗留测试,我会将其删除,并且不用任何东西来替换它.
除非小心的编码标准禁止它,否则我断言有记录的先决条件.违反已公布合同的破碎代码需要立即彻底失败.
如果null是由运行时故障引起的,而不是由于代码损坏,我抛出.fopen失败和malloc失败(虽然我很少在C++中使用它们)属于这一类.
我不会尝试从分配失败中恢复.Bad_alloc被main()捕获.
如果null测试是针对作为我的类的协作者的对象,我重写代码以通过引用获取它.
如果协作者确实可能不存在,我使用Null对象 设计模式创建一个占位符,以明确定义的方式失败.