25

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

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

4

16 回答 16

32

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

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

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

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

现在我推荐

Contract.Requires(customer != null);

然后,您可以启用运行时检查系统,该系统将尽早发现这些缺陷,引导您诊断和纠正有缺陷的代码。但是不要让我给你这样的印象,即代码契约只是替换参数 null 异常的一种奇特方式。他们比这更强大。使用 Microsoft 代码合同,您还可以运行静态检查器,并要求它调查您的代码中可能出现空引用异常的站点。静态检查器需要更多经验才能轻松使用。我不会首先向初学者推荐它。但是请随意尝试并亲自看看。

空引用错误的普遍性研究

关于空引用错误是否是一个重大问题,在这个线程中有一些争论。下面是一个冗长的答案。对于那些不想涉水的人,我会总结一下。

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

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

关于您的评论“代码中的绝大多数错误都是空引用异常”,我相信这是限定词“绝大多数”会引起一些分歧。短语“绝大多数”表明,可能 70-90% 的故障都将空引用异常作为根本原因。这对我来说似乎太高了。我更喜欢引用 Microsoft Spec# 的研究。Mike Barnett、K. Rustan M. Leino 和 Wolfram Schulte 在他们的文章 The Spec# programming system: An overview 中。在 CASSIS 2004 中,LNCS 卷。3362,施普林格,2004,他们写道

1.0 非 Null 类型 现代程序中的许多错误都表现为 null 取消引用错误,这表明编程语言提供区分可能评估为 null 的表达式和肯定不会评估为 null 的表达式的能力的重要性(对于一些实验证据,见[24, 22])。事实上,我们希望消除所有 null 取消引用错误。

对于熟悉这项研究的 Microsoft 人员来说,这可能是一个来源。这篇文章可在 Spec# 网站上找到。

我复制了下面的参考文献 22 和 24,并为您提供了 ISBN。

  • Manuel Fahndrich 和 K. Rustan M. Leino。在面向对象的语言中声明和检查非空类型。在 2003 年 ACM 面向对象编程、系统、语言和应用程序会议论文集中,OOPSLA 2003,第 38 卷,第 11 期,SIGPLAN 通知,第 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 卷,第 5 期在 SIGPLAN 通知中,第 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 上查看我的博客,该博客主要致力于通过使用合约编程来讨论软件质量。

于 2010-02-01T05:41:22.343 回答
21

除了上述(Null Objects,Empty Collections)之外,还有一些通用技术,即 C++ 的 Resource Acquisition is Initialization (RAII) 和 Eiffel 的 Design By Contract。这些归结为:

  1. 使用有效值初始化变量。
  2. 如果一个变量可以为 null,则要么检查 null 并将其视为特殊情况,要么期待 null 引用异常(并处理它)。断言可用于测试开发构建中是否违反合同。

我见过很多这样的代码:

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

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

如果这是您的代码库中的问题,那么有必要了解每种情况下 null 代表什么:

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

在实践中,这种设计级别的清晰标准并不重要,需要努力和自律才能始终如一地应用于您的代码库。

于 2009-12-22T01:03:18.307 回答
7

你没有。

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

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

到处都是(大部分 .Net 框架都是这样做的),因此当您搞砸时,您会获得更多信息性的诊断(不过,堆栈跟踪甚至更有价值,并且 NRE 也提供了这一点)。但你仍然只是以一个例外结束。

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

但归根结底,您只需要正确编码即可。

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

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

于 2009-12-22T00:50:25.910 回答
5

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

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

于 2009-12-22T00:27:50.230 回答
5

使用空对象模式是这里的关键。

确保在未填充集合时要求集合为空,而不是 null。当空集合可以使用时使用空集合会令人困惑并且通常是不必要的。

最后,我尽可能让我的对象在构造时断言非空值。这样我以后就不用怀疑值是否为空,并且只需要在必要的地方执行空检查。对于我的大多数字段和参数,我可以根据之前的断言假设值不为空。

于 2009-12-22T00:22:05.187 回答
4

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

于 2009-12-22T00:20:24.783 回答
4

我见过的最常见的空引用错误之一是来自字符串。会有一个检查:

if(stringValue == "") {}

但是,该字符串确实为空。它应该是:

if(string.IsNullOrEmpty(stringValue){}

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

于 2009-12-22T00:23:45.750 回答
3

一种方法是尽可能使用空值对象(又名空对象模式)。这里有更多细节

于 2009-12-22T00:18:19.063 回答
2

Good code analysis tools can help here. Good unit tests can also help if you're using tools that consider null as a possible path through your code. Try throwing that switch in your build settings that says "treat warnings as errors" and see if you can keep the # of warnings in your project = 0. You may find the warnings are telling you a lot.

One thing to keep in mind is that it may be a good thing that you are throwing a null - reference exception. Why? because it may mean that code that should have executed did not. Initializing to default values is a good idea, but you should be careful that you don't end up hiding a problem.

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

Alright, so this may look ok, but depending on your business rules, this may be a problem. Sure, you'll never throw a null reference, but maybe your User table should never be empty? Do you want your app to be spinning in place, generating support calls from users saying "it's just a blank screen", or do you want to raise an exception that might get logged somewhere and raise an alert quickly? Don't forget to validate what you're doing as well as 'handling' exceptions. This is one of the reasons why some are loathe to take nulls out of our languages... it makes it easier to find the bugs even though it may cause some new ones.

Remember: Handle exceptions, don't hide them.

于 2009-12-22T01:45:39.543 回答
2

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

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

于 2009-12-22T00:20:58.157 回答
2

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

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

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

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

您更愿意拥有什么,入口方法中的 ArgumentNullException,或者它内部的一个晦涩的错误?你越远离错误的根源,就越难追踪它。

于 2009-12-22T01:05:12.077 回答
1

明码解决方案

您始终可以创建一个结构,通过将变量、属性和参数标记为“不可为空”来帮助更早地捕获空引用错误。这是一个按照工作方式在概念上建模的示例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

于 2016-03-08T01:49:20.500 回答
0

如果存在可以替换 null 的合法对象,则可以使用Null Object 模式Special Case 模式。

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

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

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

于 2015-06-10T09:13:29.980 回答
0

在没有适当的“其他情况”的情况下成功避免 null 意味着现在您的程序不会失败,但也不会更正。除非整个 java api 返回 optional,否则 Optional 也无济于事,但是到那时,您将被迫到处检查任何内容,就好像到处检查 null 一样。毕竟这没什么区别。

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

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

于 2021-02-10T14:28:28.140 回答
0

可以提供帮助的工具

还有几个库可以提供帮助。上面提到了 Microsoft 代码合同。

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

还有PostSharp可以让你只使用这样的属性:

public void DoSometing([NotNull] obj)

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

Fody 代码编织项目有一个用于实现空保护的插件。

于 2016-03-08T01:52:31.957 回答
-1

NullReferenceException 可以在程序集中找不到方法时显示,例如 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(string ChildName,int count) 更新 MiniSportsCar 组件

于 2017-07-16T23:03:42.607 回答