多less空值检查就足够了?
什么是什么是什么时候没有必要检查一个空值的准则?
最近我一直在努力的很多inheritance代码都有一些空的检查。 对平凡的函数进行空值检查,对调用非空返回值的API调用进行空检查等。在某些情况下,空检查是合理的,但是在很多地方,空值不是合理的期望值。
我听说过一些论据,从“你不能相信其他代码”到“总是防守”到“直到语言保证我是一个非空值,我总是要去检查”。 我当然同意这些原则中的许多原则,但是我发现过多的空值检查会导致通常违反这些原则的其他问题。 顽强的空检查真的值得吗?
通常情况下,我已经观察到过多的空值检查代码实际上质量较差,而不是质量较高。 大部分代码似乎都非常关注空值检查,因此开发人员忽略了其他重要的特性,例如可读性,正确性或exception处理。 特别是,我看到很多代码忽略了std :: bad_allocexception,但对new
进行了空检查。
在C ++中,由于解引用空指针的不可预知的行为,我在某种程度上理解了这一点; 在Java,C#,Python等中,空解除引用是比较优雅的处理。我刚才看到了一些缺乏警惕的空值检查的例子吗?或者真的有这个东西吗?
这个问题的目的是语言不可知的,虽然我主要感兴趣的是C ++,Java和C#。
我已经看到的一些空检查的例子似乎过多,包括:
这个例子似乎是占了非标准的编译器,因为C ++规范说,失败的新引发exception。 除非您明确支持不符合规范的编译器,否则这是否有意义? 在Java或C#(甚至是C ++ / CLR)这样的托pipe语言中,这是否有意义?
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时,只有合同要求非空值? 为什么一个空检查比断言更可取?
(显然,在您的公共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; }
首先要注意的是,这是合同检查的一个特例:您正在编写的代码除了在运行时validation是否符合文档化的合同之外,什么也不做。 失败意味着某处的某些代码有问题。
对于实施一个更普遍有用的概念的特殊情况,我总是有些怀疑。 契约检查是有用的,因为它在第一次跨越API边界时捕获编程错误。 关于空值有什么特别之处,意味着它们是您要检查的合约的唯一部分? 仍然,
关于inputvalidation的主题:
null在Java中是特殊的:大量的Java API被写入,使得null是唯一无效的值,它甚至可以传递给给定的方法调用。 在这种情况下,一个空检查“完全validation”input,所以支持合同检查的全部论点都适用。
另一方面,在C ++中,由于几乎所有的地址都不是正确types的对象,所以NULL仅仅是一个指针参数可以采用的无效值,只是接近2 ^ 32(在新的体系结构上是2 ^ 64)之一。 除非在该types的所有对象中都有一个列表,否则不能“完全validation”您的input。
那么问题是,NULL是一个足够普遍的无效input来得到(foo *)(-1)
得不到的特殊处理?
与Java不同,字段不会自动初始化为NULL,所以垃圾未初始化的值与NULL一样合理。 但有时C ++对象的指针成员显式为NULL-inited,意思是“我还没有一个”。 如果您的调用者这样做,那么有一个重大的程序错误类可以通过NULL检查来诊断。 对于他们来说,一个exception可能比他们没有源代码的库中的页面错误更容易。 所以,如果你不介意代码膨胀,这可能会有所帮助。 但是你应该考虑的是你的调用者,而不是你自己 – 这不是防御性的编码,因为它只是“抵御”NULL而不是反对(foo *)( – 1)。
如果NULL不是有效的input,你可以考虑通过引用而不是指针来获取参数,但是很多编码风格不赞成非const引用参数。 而如果调用者通过你* fooptr,其中fooptr是NULL,那么它没有任何好处。 你想要做的是在函数签名中join更多的文档,希望你的调用者更可能会认为“嗯,可能在这里是空的? 当他们不得不明确地解除引用时,比把它作为指针传递给你。 它只能走得这么远,但就目前而言,这可能会有所帮助。
我不知道C#,但我明白这就像Java中的引用保证有有效的值(至less在安全的代码中),但不像Java那样,并不是所有的types都有NULL值。 所以我会猜测,空检查很less值得:如果你在安全的代码,那么不要使用可空types,除非null是一个有效的input,如果你在不安全的代码,那么相同的推理适用于在C ++中。
关于输出validation的主题:
出现类似的问题:在Java中,您可以通过了解其types来“完全validation”输出,并且该值不为空。 在C ++中,你不能用NULL检查“完全validation”输出 – 对于你所知道的,函数返回一个指向刚刚解开的栈上的对象的指针。 但是,如果由于被调用者代码的作者通常使用的构造,NULL是一个常见的无效返回,那么检查它将会有所帮助。
在所有情况下:
使用断言而不是“真正的代码”来检查合约,如果可能的话 – 一旦你的应用程序正在工作,你可能不希望每个调用者的代码膨胀检查所有的input,每个调用者检查其返回值。
在编写可移植到非标准C ++实现的代码的情况下,而不是检查null的问题中的代码,也捕获exception,我可能会有这样的function:
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的时候需要做一些特殊的事情的时候,或者当我想说清楚的时候,我会写空的检查。 否则我不介意得到空引用exception,因为这也是相当清楚发生了什么事情。
-
当处理来自其他人框架的返回数据时,我只在可能的时候检查null,并返回null。 如果他们的合同说不返还空位,我不会做检查。
如果您编写代码和合同,您有责任根据合同使用它,并确保合同是正确的。 如果你说“返回一个非null”x,那么调用者不应该检查null。 如果那个引用/指针出现空指针exception,那么你的合约是不正确的。
在使用不可信任的库或没有适当的合同时,空检查应该是极端的。 如果是你的开发团队的代码,强调合同不能被破坏,并且错误发生时跟踪错误地使用合同的人。
其中的一部分取决于代码的使用方式 – 例如,它只是一个项目中可用的方法而不是公共API。 API错误检查需要比断言更强的东西。
所以在一个支持unit testing的项目中,这样做很好:
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 ++。
- 我从来没有testing新的返回值,因为所有的实现我使用throw bad_alloc失败。 如果我在我正在处理的任何代码中看到新的返回null的遗留testing,那么我将它剪掉,不要用任何东西代替它。
- 除非小心谨慎的编码标准禁止,否则我声明有文件logging的先决条件。 违反已发布合同的破碎代码需要立即大幅失败。
- 如果null是由于不是由于破坏代码而导致的运行时失败而引起的,那么我就抛出。 fopen失败和malloc失败(尽pipe我很less使用它们在C ++中)将属于这个类别。
- 我不试图从分配失败中恢复。 Bad_alloc被main()捕获。
- 如果空testing是针对我class的协作者的对象,我重写代码以引用它。
- 如果协作者确实可能不存在,我使用Null对象devise模式来创build一个占位符以明确的方式失败。
一般而言,NULL检查是不好的,因为它会为代码可testing性添加一个小的否定标记。 随着NULL检查无处不在,你不能使用“传递null”技术,它会打你unit testing。 方法的unit testing比空检查更好。
在这个问题和unit testing一般由米斯科Hevery在http://www.youtube.com/watch?v=wEhu57pih5w&feature=channel看看体面的介绍;
较早版本的Microsoft C ++(可能还有其他版本)没有通过new引发失败的分配exception,但返回了NULL。 必须在符合标准和旧版本中运行的代码将在第一个示例中指出冗余检查。
使所有失败的分配遵循相同的代码path将是更干净的:
if(obj==NULL) throw std::bad_alloc();
众所周知,有程序导向的人(注重正确的做事方式)和注重结果的人(得到正确的答案)。 我们大多数人都在中间的某个地方。 看起来你已经发现了一个面向过程的局限性。 这些人会说:“除非你完全了解事情,否则任何事情都是可能的,所以要做好准备。” 对他们来说,你看到的是正确的。 对他们来说,如果你改变它们,他们会担心,因为鸭子不是全部排好队。
当处理别人的代码时,我试着确保我知道两件事情。
1.程序员的意图
2.他们为什么按照他们的方式编写代码
对于跟进typesA的程序员,也许这有帮助。
所以“多less就足够了”最终成为一个社会问题,就像一个技术问题 – 没有一致的方法来衡量它。
(它也使我疯狂。)
就个人而言,我认为绝大多数情况下,无效testing是不必要的。 如果新的失败或malloc失败你有更大的问题和恢复的机会是大约为零,如果你不写一个记忆检查! 同样,nulltesting在开发阶段隐藏了很多bug,因为“null”子句通常只是空的而什么都不做。
当你可以指定正在使用哪个编译器时,系统函数(如“new”检查null)是代码中的一个错误。 这意味着你将复制error handling代码。 重复的代码往往是错误的来源,因为经常有人得到改变,其他人则不会。 如果你不能指定编译器或编译器版本,你应该更加防御。
至于内部function,你应该指定合同,并确保通过unit testing来执行合同。 我们在代码中遇到了一个问题,在我们抛出一个exception的时候,或者在我们的数据库中缺less对象的情况下返回null。 这只是使api的调用者感到困惑,所以我们通过了整个代码库并使其一致,并删除了重复的检查。
重要的事情(恕我直言)是没有重复的错误逻辑,其中一个分支将永远不会被调用。 如果你永远不能调用代码,那么你不能testing它,你永远不会知道它是否被破坏。
我会说这取决于你的语言,但是我使用Resharper与C#,它基本上是它的方式告诉我“这个引用可以为null”,在这种情况下,我添加一个支票,如果它告诉我“这将永远是真实的“为”if(null!= oMyThing && ….)“然后我听它不testing为null。
是否检查null与否很大程度上取决于具体情况。
例如,在我们的商店中,我们检查参数给我们在方法中为null创build的方法。 简单的原因是,作为原程序员,我很清楚这个方法应该做什么。 即使文件和要求不完整或不尽如人意,我也了解情况。 一个后来负责维护的程序员可能不了解上下文,错误地认为传递null是无害的。 如果我知道null将是有害的,我可以预见有人可能会过零,我应该采取简单的步骤,确保该方法以优雅的方式作出反应。
public MyObject MyMethod(object foo) { if (foo == null) { throw new ArgumentNullException("foo"); } // do whatever if foo was non-null }
当我看到NULL的时候我只知道该怎么做,只检查NULL。 “知道该怎么做”的意思是“知道如何避免崩溃”或“知道除了崩溃的位置之外还要告诉用户什么”。 例如,如果malloc()返回NULL,我通常没有select,只能中止程序。 另一方面,如果fopen()返回NULL,我可以让用户知道无法打开的文件名,可能是errno。 如果find()返回end(),我通常知道如何继续而不会崩溃。
较低级别的代码应该检查更高级别代码的使用。 通常这意味着检查参数,但这可能意味着检查来自upcalls的返回值。 上电参数不需要检查。
其目的是以即时和明显的方式捕捉错误,并以不存在的代码logging合同。
我不认为这是错误的代码。 相当数量的Windows / Linux API调用在某种失败时返回NULL。 所以,当然,我会以API指定的方式检查失败。 通常我把控制stream传递给某种方式的错误模块,而不是复制error handling代码。
如果我收到一个不能被语言保证的指针,它不是null,而是要以一种空的方式去引用它,或者把我的函数放在我说我不会产生NULL的地方,检查NULL。
这不仅仅是NULL,函数应该检查前置条件和后置条件。
如果一个给我指针的函数合同说不会产生空值,那也没有关系。 我们都做错误。 有一个很好的规则,一个程序应该提前和经常失败,所以不是将错误传递给另一个模块,而是让它失败,我将失败。 testing时使事情变得更容易debugging。 而且在关键的系统中,保持系统健全也更容易。
此外,如果一个exception转义为main,堆栈可能不会被汇总,从而阻止析构函数的运行(请参阅terminate()上的C ++标准)。 这可能是严重的。 因此,不要选中bad_alloc可能比看起来更危险。
失败与断言与失败与运行时错误是一个完全不同的话题。
在new()之后检查NULL,如果标准new()行为没有被改变为返回NULL而不是抛出似乎已经过时。
还有一个问题,就是即使malloc返回了一个有效的指针,它也不意味着你已经分配了内存并且可以使用它。 不过那是另一回事了。
我的第一个问题是,它会导致代码乱七八糟的检查和类似的东西。 这会伤害到可读性,而且我甚至会说,这会损害可维护性,因为如果你正在编写一段代码,其中某个引用真的不应该为null,那么很容易忘记一个空检查。 而且你只知道在一些地方会失去空的支票。 这实际上使得debugging比需要更困难。 如果原来的exception没有被捕获,并被错误的返回值所取代,那么我们将获得一个有价值的exception对象,并提供丰富的堆栈跟踪。 什么是缺less的空检查给你? 一个NullReferenceException在一段代码,让你去:wtf? 这个引用不应该是null!
那么你需要开始搞清楚代码是如何被调用的,为什么这个引用可能是空的。 这可能需要很长时间,并且会严重影响debugging工作的效率。 最终你会发现真正的问题,但是很可能是它隐藏得很深,你花费了更多的时间去寻找它。
另外一个空白检查问题是,有些开发人员在得到一个NullReferenceException时,并没有真正花时间去考虑真正的问题。 我已经看到不less开发人员在发生NullReferenceException的代码上面添加一个空的检查。 太棒了,例外不再发生! 欢呼! 我们现在可以回家了! 呃…怎么回事“不,你不配,你应该脸胳膊肘”? 真正的错误可能不会导致exception,但现在你可能已经失踪或错误的行为…并没有例外! 这更加痛苦,需要更多时间来debugging。
起初,这似乎是一个奇怪的问题: null
检查是伟大的和有价值的工具。 检查new
返回null
是绝对愚蠢的。 我只是会忽略这样的事实,即有语言允许。 我敢肯定,这是有道理的,但是我真的不认为我可以处理这种现实:)所有的开玩笑,似乎你至less必须指定new
应该返回null
当没有足够的记忆。
无论如何,在适当的地方检查null
会导致更干净的代码。 我甚至可以说,永不分配函数参数的默认值是下一个逻辑步骤。 为了更进一步,在适当的地方返回空数组等,导致更简洁的代码。 除非在逻辑上有意义,否则不必担心获取null
。 空值作为错误值最好避免。
使用断言是一个非常好的主意。 特别是如果它允许您在运行时closures它们的选项。 另外,这是一个更明确的契约风格:)