什么是空引用异常(NullReferenceException),如何修复?

1869

我有一些代码,当它执行时,抛出了一个 NullReferenceException 异常,显示:

对象引用未设置为对象的实例。

这是什么意思?我该如何修复此错误?


在VS 2017中的异常助手将更有帮助,可以诊断此异常的原因--https://blogs.msdn.microsoft.com/visualstudio/2016/11/28/productivity-in-visual-studio-2017-rc/下的“新异常助手”。 - Zev Spitz
亲爱的未来访客,这个问题的答案同样适用于ArgumentNullException。如果你的问题已被关闭并列为重复的问题,并且你正在遇到ANE,请按照答案中的指示进行调试和修复问题。 - user1228
@will,ANE 只有在传递 null 作为参数时才会发生。你能给出一个 ANE 问题被关闭,并作为此问题的副本的示例吗? - John Saunders
这个问题在Meta上出现过,但我得去找链接。至于那个评论,ANE只是NRE的一种,但有人添加了一个预防性检查,而且你至少知道哪里为空(参数名已提供),所以诊断起来比直接的NRE要容易一些。 - user1228
27个回答

70
虽然其他答案已经讨论了导致NullReferenceExceptions的原因以及避免/修复此类异常的方法,但许多程序员尚未学会如何在开发过程中独立调试这些异常。在Visual Studio中,由于Visual Studio Debugger,这通常很容易。请注意保留HTML标记。
首先,请确保正确的错误被捕获 - 参见如何在VS2010中允许'System.NullReferenceException'断点调试?注1 然后,要么使用开始调试(F5)附加[VS调试器]到运行进程。有时候使用Debugger.Break可能会很有用,它会提示启动调试器。
现在,当NullReferenceException被抛出(或未处理)时,调试器将停止(记住上面的规则集?)在异常发生的行上。有时候错误很容易发现。
例如,在下面的代码中,唯一可能导致异常的代码是如果myString评估为null。这可以通过查看Watch Window或在Immediate Window中运行表达式来验证。
var x = myString.Trim();

在更高级的情况下,例如以下情况,您需要使用上述技术(监视或立即窗口)之一来检查表达式,以确定str1是否为空或str2是否为空。
var x = str1.Trim() + str2.Trim();

一旦定位到异常被抛出的位置,通常很容易倒推出空值是在哪里[错误地]引入的-
花费必要的时间来理解异常的原因。检查是否存在空表达式。检查可能导致这些空表达式的先前表达式。适当添加断点并步入程序-使用调试器。

1 如果“在抛出异常时中断”太过严格,调试器会在.NET或第三方库中的NPE上停止,可以使用Break on User-Unhandled来限制捕获的异常。另外,VS2012引入了Just My Code,我建议也启用。

如果启用了仅调试我的代码,行为会略有不同。启用了仅调试我的代码后,调试器会忽略在我的代码之外抛出并且没有通过我的代码传递的CLR常见运行时异常的first-chance异常。


64

Simon Mourier提供了以下示例::

object o = null;
DateTime d = (DateTime)o;  // NullReferenceException

当从object(或者来自于类System.ValueTypeSystem.Enum或接口类型之一)转换为除了Nullable<>以外的值类型时,进行拆箱转换(强制类型转换)会导致NullReferenceException异常。

反过来,从一个HasValue等于falseNullable<>进行装箱转换到引用类型会得到一个null引用,稍后可能会导致NullReferenceException异常。经典的例子是:

DateTime? d = null;
var s = d.ToString();  // OK, no exception (no boxing), returns ""
var t = d.GetType();   // Bang! d is boxed, NullReferenceException

有时候,拳击的方式会有所不同。例如,使用这个非通用扩展方法:
public static void MyExtension(this object x)
{
  x.ToString();
}

以下代码可能会出现问题:
DateTime? d = null;
d.MyExtension();  // Leads to boxing, NullReferenceException occurs inside the body of the called method, not here.

这些情况是由于运行时在装箱 Nullable<> 实例时使用的特殊规则引起的。


46

当实体框架中的实体类名称与Web表单代码后台文件的类名称相同时,需要添加一个特殊情况。

假设您有一个名为Contact.aspx的Web表单,其代码后台类为Contact,而您的实体名称为Contact。

当调用context.SaveChanges()时,以下代码将抛出NullReferenceException。

Contact contact = new Contact { Name = "Abhinav"};
var context = new DataContext();
context.Contacts.Add(contact);
context.SaveChanges(); // NullReferenceException at this line

为了完整起见,DataContext类

public class DataContext : DbContext 
{
    public DbSet<Contact> Contacts {get; set;}
}

以及联系实体类。有时实体类是部分类,这样您就可以在其他文件中扩展它们。

public partial class Contact 
{
    public string Name {get; set;}
}

当实体类和后台代码类在同一个命名空间中时,会出现此错误。为了解决这个问题,请将实体类或 Contact.aspx 的后台代码类重命名。

原因我仍然不确定原因。但是,每当任何实体类扩展 System.Web.UI.Page 时,都会发生此错误。

有关讨论,请参见DbContext.saveChanges() 中的 NullReferenceException


45

另一种可能会出现此异常的通用情况涉及单元测试期间模拟类。无论使用哪种模拟框架,您都必须确保正确模拟了类层次结构的所有适当级别。特别是,测试代码引用的HttpContext的所有属性都必须被模拟。

请参见"在测试自定义AuthorizationAttribute时引发NullReferenceException",其中有一个相对冗长的例子。


44
我有一个不同的观点来回答这个问题。这种回答通常是“还有什么其他方法可以避免它?”。
当在不同的层之间工作时,例如在MVC应用程序中,控制器需要调用业务操作的服务。在这种情况下,可以使用依赖注入容器来初始化服务,以避免空引用异常。这意味着你不需要担心检查null,只需从控制器调用服务,就好像它们总是可用(并已初始化)作为单例或原型。
public class MyController
{
    private ServiceA serviceA;
    private ServiceB serviceB;

    public MyController(ServiceA serviceA, ServiceB serviceB)
    {
        this.serviceA = serviceA;
        this.serviceB = serviceB;
    }

    public void MyMethod()
    {
        // We don't need to check null because the dependency injection container 
        // injects it, provided you took care of bootstrapping it.
        var someObject = serviceA.DoThis();
    }
}

6
这只处理单个场景,即未初始化依赖项的情况。这是NullReferenceException的少数情况。大多数情况下是由于开发人员对对象工作方式的简单误解。其次最常见的是其他情况,其中开发人员假设对象将自动初始化。 - John Saunders
依赖注入通常不被使用,以避免NullReferenceException。我不认为你找到了一个普遍的场景。无论如何,如果你编辑你的回答更符合https://dev59.com/O2445IYBdhLWcg3w3N6z#15232518的风格,那么我会取消踩。 - John Saunders

42

在“我该怎么做”这个问题上,可能会有很多答案。

防止开发过程中出现此类错误条件的一种更“正式”的方法是在代码中应用契约式设计。这意味着您需要在开发过程中设置类不变量,并且甚至可以在系统中设置函数/方法前置条件后置条件

简而言之,类不变量确保在正常使用时将存在一些约束条件,不会违反这些条件(因此,类将不会进入不一致状态)。前置条件意味着作为函数/方法输入的数据必须遵循一些设定的约束条件,并且永远不会违反它们,而后置条件意味着函数/方法输出必须再次遵循设置的约束条件,而且永远不会违反它们。 在没有漏洞的程序执行期间,契约条件绝不能被违反,因此在调试模式下检查契约式设计,同时在发布时禁用,以最大化开发系统性能。

这样,您就可以避免由于违反设定约束条件而导致的NullReferenceException情况。例如,如果您在类中使用对象属性X,然后尝试调用其方法并且X具有空值,那么这将导致NullReferenceException

public X { get; set; }

public void InvokeX()
{
    X.DoSomething(); // if X value is null, you will get a NullReferenceException
}

但如果您将“属性X永远不应该具有空值”设置为方法前置条件,那么您可以防止上述情况发生:

//Using code contracts:
[ContractInvariantMethod]
protected void ObjectInvariant() 
{
    Contract.Invariant(X != null);
    //...
}

因此,Code Contracts 项目适用于 .NET 应用程序。

或者,可以使用 assertions 来应用按合同设计。

更新:值得一提的是,该术语是由 Bertrand Meyer 在他设计 Eiffel 编程语言时创造的


2
我想添加这个内容,因为没有人提到过,而且只要它作为一种方法存在,我的意图就是丰富这个主题。 - Nick Louloudakis
2
我认为这是一个有价值的补充,因为这是一个非常受关注的主题。我以前听说过代码契约,这是一个很好的提醒要考虑使用它们。 - VoteCoffee

40

当我们试图访问空对象的属性或者字符串值变为空并且我们试图访问字符串方法时,会抛出一个NullReferenceException

例如:

  1. 当访问一个空字符串的方法时:

string str = string.Empty;
str.ToLower(); // throw null reference exception
  • 当访问 null 对象的属性时:

  • Public Class Person {
        public string Name { get; set; }
    }
    Person objPerson;
    objPerson.Name  /// throw Null refernce Exception 
    

    3
    这是不正确的。String.Empty.ToLower()不会引发空引用异常。它代表一个实际的字符串,尽管是一个空字符串(即 "")。由于这里有一个对象可调用 ToLower() 方法,因此在那里抛出空引用异常是没有意义的。 - Kjartan

    35

    TL;DR:尝试使用Html.Partial而不是Renderpage


    当我尝试通过传递模型来在一个视图中呈现另一个视图时,出现了Object reference not set to an instance of an object的错误:

    @{
        MyEntity M = new MyEntity();
    }
    @RenderPage("_MyOtherView.cshtml", M); // error in _MyOtherView, the Model was Null
    

    调试显示MyOtherView内的模型为空。直到我将其更改为:

    @{
        MyEntity M = new MyEntity();
    }
    @Html.Partial("_MyOtherView.cshtml", M);
    

    而且它起作用了。

    此外,我一开始没有使用Html.Partial是因为Visual Studio有时会在不同构造的foreach循环内部下方波浪线看起来像错误,即使它不是真的错误:

    @inherits System.Web.Mvc.WebViewPage
    @{
        ViewBag.Title = "Entity Index";
        List<MyEntity> MyEntities = new List<MyEntity>();
        MyEntities.Add(new MyEntity());
        MyEntities.Add(new MyEntity());
        MyEntities.Add(new MyEntity());
    }
    <div>
        @{
            foreach(var M in MyEntities)
            {
                // Squiggly lines below. Hovering says: cannot convert method group 'partial' to non-delegate type Object, did you intend to envoke the Method?
                @Html.Partial("MyOtherView.cshtml");
            }
        }
    </div>
    

    但我能够在这个“错误”的情况下无问题地运行应用程序。通过将foreach循环的结构更改为以下方式,我成功摆脱了这个错误:

    @foreach(var M in MyEntities){
        ...
    }
    

    尽管我有一种感觉,那就是因为Visual Studio误读了&和括号。


    你需要使用 Html.Partial,而不是 @Html.Partial - John Saunders
    另外,请显示抛出异常的行以及原因。 - John Saunders
    错误发生在MyOtherView.cshtml中,由于模型未被正确发送(它是“Null”),因此我知道错误是由于我发送模型的方式有误。 - Travis Heeter

    24

    你能做些什么?

    这里有很多好的答案,解释了空引用是什么以及如何调试它。但是在如何预防此问题或至少使其更容易捕获方面却很少有涉及。

    检查参数

    例如,方法可以检查不同的参数是否为null并抛出一个ArgumentNullException,这是显然为此目的而创建的异常。

    ArgumentNullException的构造函数甚至接受参数的名称和消息,因此您可以准确地告诉开发人员问题所在。

    public void DoSomething(MyObject obj) {
        if(obj == null) 
        {
            throw new ArgumentNullException("obj", "Need a reference to obj.");
        }
    }
    

    使用工具

    还有一些库可以帮助你。例如,“Resharper”可以在你编写代码时提供警告,特别是如果你使用他们的属性:“NotNullAttribute”。

    有“Microsoft Code Contracts”,在那里你使用类似于 Contract.Requires(obj != null) 的语法,这将为你提供运行时和编译检查:介绍代码契约

    还有“PostSharp”,只需像这样使用属性:

    public void DoSometing([NotNull] obj)
    

    通过这样做并将PostSharp作为您的构建过程的一部分,obj 将在运行时检查是否为空。请参见:PostSharp空检查 常规代码解决方案 或者,您始终可以使用普通旧代码编写自己的方法。例如,这是一个结构体,您可以使用它来捕获空引用。它的设计理念与 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

    相关语言特性

    C# 6.0引入了“null-conditional operator”这一特性,有助于解决这个问题。使用这个特性,您可以引用嵌套对象,如果其中任何一个对象是null,整个表达式就返回null

    有时候,这可以减少您需要进行的空值检查的数量。语法是在每个点之前加上一个问号。例如,看下面的代码:

    var address = country?.State?.County?.City;
    

    假设country是类型为Country的对象,它有一个名为State的属性等等。如果countryStateCountyCity为空,则address will benull。因此,您只需要检查address是否为空。

    这是一个很棒的功能,但它提供的信息较少。它并没有明确告诉你这4个中哪一个是null。

    类似于Nullable的内置结构?

    C#中有一个很好的简写方式Nullable<T>,您可以通过在类型后面加上问号来使其可空,如int?

    如果C#有像上面的NotNull<T>结构,并且有类似的简写方式,比如感叹号(!),那就太好了,这样您就可以编写类似于public void WriteName(Person! person)的代码了。


    2
    永远不要抛出NullReferenceException。 - John Saunders
    2
    NullReferenceException是由CLR抛出的。它意味着发生了对null的引用。这并不意味着除非你聪明地先进行检查,否则会发生对null的引用。 - John Saunders
    对于一个基础问题来说,这是个很好的答案。当你的代码出现问题时,情况并不那么糟糕。但当问题来自于你所依赖的商业第三方库的深处,并且客户支持一直坚称问题一定是由你的代码引起的时,情况就非常可怕了。你也不能完全确定问题不是出在你的代码上,整个项目都陷入了停滞状态...我认为这可能适合作为我的墓志铭:"对象引用未设置到对象的实例"。 - Darrel Lee

    15

    使用 C# 6 中的 Null-conditional 运算符可以更加简洁地处理 NullReferenceException,并且写更少的代码来处理 null 检查。

    它用于在执行成员访问 (?.) 或索引 (?[) 操作之前测试是否为 null。

    示例

      var name = p?.Spouse?.FirstName;
    

    它等同于:

        if (p != null)
        {
            if (p.Spouse != null)
            {
                name = p.Spouse.FirstName;
            }
        }
    

    如果p为null或p.Spouse为null,则名称将为null。

    否则,变量名称将被赋值为p.Spouse.FirstName的值。

    更多详情请参见:Null-conditional Operators


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接