避免空引用exception

显然,代码中的绝大多数错误都是空引用exception。 是否有任何一般技术可以避免遇到空引用错误?

除非我弄错了,否则我知道在F#这样的语言中,不可能有空值。 但那不是问题,我问如何避免C#等语言中的空引用错误。

当向用户显示空引用exception时,这表示代码中由于开发人员的错误而导致的缺陷。 以下是有关如何防止这些错误的一些想法。

我对那些关心软件质量并且也在使用.NET编程平台的人的最佳建议是安装和使用Microsoft代码合同( http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx ) 。 它包括执行运行时检查和静态validation的function。 将这些合同构建到代码中的基本function包含在.NET框架的4.0版本中。 如果您对代码质量感兴趣,并且听起来像您,您可能真的很喜欢使用Microsoft代码合同。

使用Microsoft代码协定,您可以通过添加类似“Contract.Requires(customer!= null);”的前提条件来保护您的方法免受空值的影响。 添加这样的前提条件相当于许多其他人在上述评论中推荐的做法。 在代码合同之前,我会建议你做这样的事情

 if (customer == null) {throw new ArgumentNullException("customer");} 

现在我推荐

 Contract.Requires(customer != null); 

然后,您可以启用运行时检查系统,该系统将尽早捕获这些缺陷,从而引导您诊断和纠正有缺陷的代码。 但是,不要让我给你的印象是代码契约只是一种替换参数nullexception的奇特方式。 它们比那更强大。 使用Microsoft代码协定,您还可以运行静态检查程序,并要求它调查代码中可能发生空引用exception的可能站点。 静态检查器需要更多经验才能轻松使用。 我不会先为初学者推荐它。 但随意尝试一下,亲眼看看吧。

关于空参考误差的研究

关于空引用错误是否是一个重要问题,在这个post中存在一些争论。 一个冗长的答案如下。 对于那些不想涉及的人,我将总结一下。

  • 微软在Spec#和代码合同项目中程序正确性方面的领先研究人员认为这是一个值得解决的问题。
  • Bertrand Meyer博士和ISE软件工程师团队开发并支持Eiffel编程语言,他们也认为这是一个值得解决的问题。
  • 在我自己开发普通软件的商业经验中,我经常看到空引用错误,我想在我自己的产品和实践中解决这个问题。

多年来,微软一直致力于旨在提高软件质量的研究。 他们的一项努力是Spec#项目。 我认为.NET 4.0框架最令人兴奋的发展之一就是引入了Microsoft代码契约,这是Spec#研究团队早期工作的产物。

关于你的评论“代码中的绝大多数错误都是空引用exception”,我认为它是“绝大多数”的限定符会引起一些分歧。 短语“绝大多数”表明大概有70-90%的故障将空引用exception作为根本原因。 这对我来说似乎太高了。 我更喜欢引用Microsoft Spec#的研究。 在他们的文章The Spec#programming system:Mike Balnett,K。Rustan M. Leino和Wolfram Schulte的概述中。 在CASSIS 2004,LNCS vol。 3362,Springer,2004,他们写道

1.0非空类型现代程序中的许多错误表现为空解除错误,这表明编程语言的重要性提供了区分可能评估为空的表达式和那些肯定不会出现的表达式的能力(对于某些实validation据,见[24,22])。 实际上,我们希望根除所有null解除引用错误。

对于熟悉此研究的Microsoft人员来说,这可能是一个很好的来源。 本文可在Spec#站点获得。

我复制了下面的参考文献22和24,并为了您的方便而包含了ISBN。

  • Manuel Fahndrich和K. Rustan M. Leino。 以面向对象的语言声明和检查非null类型。 在2003年ACM会议中关于面向对象编程,系统,语言和应用的会议论文集,OOPSLA 2003,第38卷,SIGPLAN通告中的第11期,第302-312页。 ACM,2003年11月.isbn = {1-58113-712-5},

  • Cormac Flanagan,K。Rustan M. Leino,Mark Lillibridge,Greg Nelson,James B. Saxe和Raymie Stata。 Java的扩展静态检查。 在2002年ACM SIGPLAN会议编程语言设计和实现会议(PLDI)的会议录,第37卷,SIGPLAN通告第5页,第234-245页。 ACM,2002年5月。

我查看了这些参考文献 第一个参考文献表明他们确实在一些实验中检查了他们自己的代码是否存在可能的空参 他们不仅找到了几个,而且在很多情况下,识别潜在的零参考表明设计存在更广泛的问题。

第二个引用没有提供任何关于空引用错误是问题的断言的具体证据。 但作者确实表示,根据他们的经验,这些空引用错误是软件缺陷的重要来源。 然后,本文继续解释他们如何试图根除这些缺陷。

我还记得ISE在最近发布的Eiffel上的公告中看到了一些相关信息。 他们将这个问题称为“无效安全”,就像Bertrand Meyer博士启发或开发的许多事情一样,他们对这个问题有着雄辩和教育性的描述,以及他们如何用他们的语言和工具来防止这个问题。 我建议您阅读他们的文章http://doc.eiffel.com/book/method/void-safety-background-definition-and-tools以了解更多信息。

如果您想了解有关Microsoft代码合同的更多信息,最近会出现大量文章。 您还可以在http:SLASH SLASH codecontracts.info上查看我的博客,该代码主要用于通过使用合同编程来讨论软件质量。

除了上述(空对象,空集合)之外,还有一些通用技术,即资源获取是来自C ++的初始化(RAII)和来自Eiffel的Design By Contract。 归结为:

  1. 使用有效值初始化变量。
  2. 如果变量可以为null,则检查null并将其视为特殊情况或期望空引用exception(并处理该exception)。 断言可用于测试开发构建中的合同违规。

我见过很多看起来像这样的代码:

if((value!= null)&&(value.getProperty()!= null)&& … &&(… doSomethingUseful())

很多时候这是完全没必要的,大多数测试可以通过更严格的初始化和更严格的合同定义来删除。

如果这是您的代码库中的问题,那么有必要在每种情况下理解null表示的内容:

  1. 如果null表示空集合,请使用空集合。
  2. 如果null表示exception情况,则抛出exception。
  3. 如果null表示意外未初始化的值,则显式初始化它。
  4. 如果null表示合法值,则测试它 – 或者甚至更好地使用执行null操作的NullObject。

实际上,这种设计级别的清晰度标准非常重要,需要付出努力和自律才能始终如一地应用于您的代码库。

你没有。

或者更确切地说,尝试在C#中“阻止”NRE并没有什么特别之处。 在大多数情况下,NRE只是某种类型的逻辑错误。 您可以通过检查参数和拥有大量代码来在接口边界处对这些进行防火墙处理

 void Foo(Something x) { if (x==null) throw new ArgumentNullException("x"); ... } 

所有的地方(大部分.Net框架都这样做),所以当你搞砸了,你会得到一个更具信息性的诊断(尽管堆栈跟踪更有价值,NRE也提供了这一点)。 但你仍然只是一个例外。

(旁白:像这样的例外 – NullReferenceException,ArgumentNullException,ArgumentException,… – 通常不应该被程序捕获,而只是意味着“这个代码的开发者,有一个错误,请修复它。”我指的是这些作为“设计时间”exception;将这些与运行时环境(例如FileNotFound)导致的真正“运行时”exception进行对比,并且可能被程序捕获和处理。)

但是在一天结束时,你只需要正确编码即可。

理想情况下,大多数NRE永远不会发生,因为’null’对于许多类型/变量来说是一个荒谬的值,理想情况下,静态类型系统会禁止’null’作为这些特定类型/变量的值。 然后编译器会阻止您引入这种类型的意外错误(排除某些类型的错误是编译器和类型系统最擅长的)。 这是某些语言和类型系统擅长的地方。

但是如果没有这些function,您只需测试代码以确保没有此类错误的代码路径(或者可能使用一些可以为您进行额外分析的外部工具)。

使用空对象模式是关键。

确保在未填充集合时要求集合为空,而不是为空。 当空集合执行时使用空集合会让人感到困惑并且通常是不必要的。

最后,我尽可能在构造时使我的对象断言为非null值。 这样我以后无疑会知道值是否为null,并且只需执行必要的空检查。 对于我的大多数字段和参数,我可以假设基于先前的断言,值不为空。

我见过的最常见的空引用错误之一是来自字符串。 会有一张支票:

 if(stringValue == "") {} 

但是,字符串实际上是空的。 它应该是:

 if(string.IsNullOrEmpty(stringValue){} 

此外,您可能过于谨慎,并在尝试访问该对象的成员/方法之前检查对象是否为空。

您可以在导致exception之前轻松检查空引用,但通常这不是真正的问题,因此您最终会抛出exception,因为代码在没有任何数据的情况下无法真正继续。

通常主要的问题不是你有一个空引用,而是你首先得到一个空引用。 如果引用不应该为null,则在没有适当引用的情况下,不应超过引用初始化的点。

一种方法是尽可能使用Null值对象(也称为Null对象模式 )。 这里有更多细节

真的,如果在你的语言中有空值,它肯定会发生。 空引用错误来自应用程序逻辑中的错误 – 因此,除非你能够避免所有这些错误,否则你必须遇到一些错误。

适当使用结构化exception处理有助于避免此类错误。

此外,unit testing可以帮助您确保代码按预期运行,包括确保值不应该是空值。

避免NullReferenceExceptions的最简单方法之一是在类构造函数/方法/属性设置器中积极检查空引用,并引起对问题的注意。

例如

 public MyClass { private ISomeDependency m_dependencyThatWillBeUsedMuchLater // passing a null ref here will cause // an exception with a meaningful stack trace public MyClass(ISomeDependency dependency) { if(dependency == null) throw new ArgumentNullException("dependency"); m_dependencyThatWillBeUsedMuchLater = dependency; } // Used later by some other code, resulting in a NullRef public ISomeDependency Dep { get; private set; } } 

在上面的代码中,如果传递null ref,您将立即发现调用代码使用的类型不正确。 如果没有空引用检查,则可以通过许多不同方式隐藏错误。

您会注意到.NET框架库几乎总是提前失败,并且通常在您提供空引用的情况下通常会失败。 由于抛出的exception明确表示“你搞砸了!” 并告诉你为什么,它使检测和纠正有缺陷的代码是一项微不足道的任务。

我听到一些开发人员的抱怨,他们说这种做法过于冗长和冗余,因为NullReferenceException就是你所需要的,但实际上我发现它有很大的不同。 如果调用堆栈很深和/或存储了参数并且其使用推迟到以后(可能在不同的线程上或以某种其他方式模糊),则尤其如此。

你更喜欢什么,入口方法的ArgumentNullException,或者它的内脏中的一个模糊的错误? 你越远离错误的来源,追踪它就越难。

好的代码分析工具可以帮到这里。 如果您使用的工具将null视为代码的可能路径,那么良好的unit testing也会有所帮助。 尝试在构建设置中抛出该开关,将“将警告视为错误”并查看是否可以保留项目中的警告数量= 0.您可能会发现警告告诉您很多。

要记住的一件事是,抛出null引用exception可能是件好事。 为什么? 因为它可能意味着应该执行的代码没有。 初始化为默认值是一个好主意,但您应该小心,不要最终隐藏问题。

 List GetAllClients() { List returnList = new List; /* insert code to go to data base and get some data reader named rdr */ for (rdr.Read() { /* code to build Client objects and add to list */ } return returnList; } 

好吧,所以这可能看起来不错,但根据您的业务规则,这可能是一个问题。 当然,你永远不会抛出空引用,但也许你的User表永远不应该是空的? 您是否希望您的应用程序在适当的位置旋转,从用户那里产生支持电话,说“它只是一个空白屏幕”,或者您是否想要提出可能会在某处记录并快速发出警报的exception? 不要忘记validation您正在做什么以及“处理”exception。 这是为什么有些人不愿意从我们的语言中取出空值的原因之一…它使得更容易找到错误,即使它可能会导致一些新的错误。

记住:处理exception,不要隐藏它们。

普通代码解决方案

您总是可以创建一个结构,通过将变量,属性和参数标记为“不可为空”来帮助捕获空引用错误。 这是一个在Nullable工作方式之后的概念模型示例:

 [System.Diagnostics.DebuggerNonUserCode] public struct NotNull where T : class { private T _value; public T Value { get { if (_value == null) { throw new Exception("null value not allowed"); } return _value; } set { if (value == null) { throw new Exception("null value not allowed."); } _value = value; } } public static implicit operator T(NotNull notNullValue) { return notNullValue.Value; } public static implicit operator NotNull(T value) { return new NotNull { Value = value }; } } 

您将使用与使用Nullable相同的方式,除非目标是完全相反 – 不允许null 。 这里有些例子:

 NotNull person = null; // throws exception NotNull person = new Person(); // OK NotNull person = GetPerson(); // throws exception if GetPerson() returns null 

NotNull隐式地从T转换为T因此您可以在任何需要它的地方使用它。 例如,您可以将Person对象传递给采用NotNull

 Person person = new Person { Name = "John" }; WriteName(person); public static void WriteName(NotNull person) { Console.WriteLine(person.Value.Name); } 

正如您在上面看到的那样,您可以通过Value属性访问基础值。 或者,您可以使用显式或隐式强制转换,您可以看到一个带有返回值的示例:

 Person person = GetPerson(); public static NotNull GetPerson() { return new Person { Name = "John" }; } 

或者您甚至可以在方法通过执行转换返回T (在本例中为Person )时使用它。 例如,以下代码就像上面的代码:

 Person person = (NotNull)GetPerson(); public static Person GetPerson() { return new Person { Name = "John" }; } 

与扩展相结合

NotNull与扩展方法结合使用,您可以涵盖更多情况。 以下是扩展方法的示例:

 [System.Diagnostics.DebuggerNonUserCode] public static class NotNullExtension { public static T NotNull(this T @this) where T : class { if (@this == null) { throw new Exception("null value not allowed"); } return @this; } } 

以下是如何使用它的示例:

 var person = GetPerson().NotNull(); 

GitHub上

为了您的参考,我在GitHub上提供了上面的代码,您可以在以下位置找到它:

https://github.com/luisperezphd/NotNull

在可以存在可以替换null的合法对象的情况下,可以使用Null对象模式和特殊情况模式 。

在无法构造此类对象的情况下,由于根本无法实现其强制操作,您可以依赖空集合,例如Map-Reduce Queries 。

另一种解决方案是Optionfunction类型 ,它是具有零个或一个元素的集合。 这样,您将有机会跳过无法执行的操作。

这些选项可以帮助您编写代码而不需要任何空引用和任何空检查。

可以提供帮助的工具

还有几个库可以提供帮助。 上面提到了Microsoft Code Contracts。

其他一些工具包括Resharper ,它可以在您编写代码时为您提供警告,尤其是在您使用其属性时: NotNullAttribute

还有PostSharp ,它允许你只使用这样的属性:

 public void DoSometing([NotNull] obj) 

通过这样做并使构建过程obj PostSharp部分将在运行时检查为null。 请参阅: PostSharp null检查

Fody代码编织项目有一个用于实现空值守卫的插件。

当在程序集中找不到方法时,可以显示NullReferenceException,对于ex m0 = mi.GetType()。GetMethod(“TellChildToBeQuiet”),其中程序集是SportsMiniCar,mi是MiniVan的实例,而TellChildToBeQuiet是程序集中的方法。 我们可以通过查看包含上述方法的程序集版本2.0.0.0放在GAC中来避免这种情况。 示例:使用参数调用方法:`

 enter code here using System; using System.Rwflection; using System.IO; using Carlibraries; namespace LateBinding { public class program { static void Main(syring[] args) { Assembly a=null; try { a=Assembly.Load("Carlibraries"); } catch(FileNotFoundException e) { Console.Writeline(e.Message); Console.ReadLine(); return; } Type miniVan=a.GetType("Carlibraries.MiniVan"); MiniVan mi=new MiniVan(); mi.TellChildToBeQuiet("sonu",4); Console.ReadLine(); } } } 

记得用TellChildToBeQuiet更新MiniSportsCar程序集(字符串ChildName,int count)