避免空引用异常

时间:2009-12-22 00:17:06

标签: c# exception nullreferenceexception

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

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

16 个答案:

答案 0 :(得分:30)

答案 1 :(得分:21)

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

  1. 使用有效值初始化变量。
  2. 如果变量可以为null,则检查null并将其视为特殊情况或期望空引用异常(并处理该异常)。断言可用于测试开发版本中的合同违规。
  3. 我看过很多看起来像这样的代码:

      

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

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

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

    1. 如果null表示空集合,请使用空集合。
    2. 如果null表示异常情况,则抛出异常。
    3. 如果null表示意外未初始化的值,请显式初始化它。
    4. 如果null表示合法值,请测试它 - 或者甚至更好地使用执行null操作的NullObject。
    5. 在实践中,设计层面的这种清晰度标准并非易事,需要付出努力和自律才能始终如一地应用于您的代码库。

答案 2 :(得分:7)

你没有。

或者更确切地说,尝试在C#中“阻止”NRE并没有什么特别之处。在大多数情况下,NRE只是某种类型的逻辑错误。您可以通过检查参数和拥有大量代码(如

)来在接口边界处对这些进行防火墙处理
void Foo(Something x) {
    if (x==null)
        throw new ArgumentNullException("x");
    ...
}

到处都是(大部分.Net Framework都这样做),所以当你搞砸了,你会得到一个稍微提供更多信息的诊断(虽然堆栈跟踪更有价值,NRE也提供了这个功能。 )。但你仍然只是一个例外。

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

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

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

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

答案 3 :(得分:5)

使用Null Object Patterns是关键。

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

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

答案 4 :(得分:5)

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

通常,主要问题不在于您有一个空引用,而是您首先获得了一个空引用。如果引用不应该为null,那么在没有正确引用的情况下,不应超过引用初始化的点。

答案 5 :(得分:4)

如果您的语言中存在空值,则必然会发生。空引用错误来自应用程序逻辑中的错误 - 所以除非你能避免所有这些错误,否则你必然会遇到一些错误。

答案 6 :(得分:4)

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

if(stringValue == "") {}

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

if(string.IsNullOrEmpty(stringValue){}

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

答案 7 :(得分:3)

一种方法是尽可能使用Null Value Objects(aka the Null Object Pattern)。有more details here

答案 8 :(得分:2)

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

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

答案 9 :(得分:2)

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

E.g。

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

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

您会注意到,.NET框架库几乎总是提前失败,并且通常如果您提供空引用而无法执行此操作。由于抛出的异常明确表示“你搞砸了!”并告诉你原因,它使检测和纠正有缺陷的代码成为一项微不足道的任务。

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

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

答案 10 :(得分:2)

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

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

List<Client> GetAllClients()
{
    List<Client> returnList = new List<Client>;
    /* 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表永远不应该是空的?您是否希望您的应用程序在适当的位置旋转,从用户那里产生“只是一个空白屏幕”的支持电话,或者您是否想要提出可能在某处记录并快速发出提醒的异常?不要忘记验证您正在做什么以及“处理”异常。这就是为什么有些人不愿意从我们的语言中取出空值的原因之一...它使得查找错误变得更容易,即使它可能会导致一些新错误。

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

答案 11 :(得分:1)

普通代码解决方案

您总是可以创建一个结构,通过将变量,属性和参数标记为&#34;而不是可空的&#34;来帮助先捕获空引用错误。以下是Nullable<T>工作方式概念性建模的示例:

[System.Diagnostics.DebuggerNonUserCode]
public struct NotNull<T> 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<T> notNullValue)
    {
        return notNullValue.Value;
    }

    public static implicit operator NotNull<T>(T value)
    {
        return new NotNull<T> { Value = value };
    }
}

您将使用与使用Nullable<T>相同的方式,但目标是完全相反 - 不允许null。以下是一些例子:

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

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

的方法
Person person = new Person { Name = "John" };
WriteName(person);

public static void WriteName(NotNull<Person> person)
{
    Console.WriteLine(person.Value.Name);
}

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

Person person = GetPerson();

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

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

Person person = (NotNull<Person>)GetPerson();

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

与分机结合

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

[System.Diagnostics.DebuggerNonUserCode]
public static class NotNullExtension
{
    public static T NotNull<T>(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

答案 12 :(得分:0)

如果可以存在可以替换null的合法对象,则可以使用Null Object patternSpecial Case pattern

如果无法构造此类对象,因为根本无法实现其强制操作,您可以依赖空集合,例如Map-Reduce Queries

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

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

答案 13 :(得分:0)

可提供帮助的工具

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

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

还有 PostSharp ,这样您就可以使用以下属性:

public void DoSometing([NotNull] obj)

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

Fody代码编织项目有一个implementing null guards.

插件

答案 14 :(得分:0)

在没有适当的“else case”的情况下成功避免 null 意味着现在您的程序不会失败,但也不会正确。 Optional 也帮不了你,除非整个 java api 都返回 optional ,但到那时,你不得不到处检查什么,就像到处检查 null 一样。毕竟这没什么区别。

未来人们可能会发明另一个对象“Falsable”,以避免不检查就返回false!哈哈

只有理解逻辑并根据需要检查才能帮助您。不可选。这只是虚假的安全。

答案 15 :(得分:-1)

当在程序集中找不到方法时,可以显示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)