什么是 NullReferenceException,我该如何解决?

分享于2022年07月17日 .net c# null nullreferenceexception vb.net 问答
【问题标题】:什么是 NullReferenceException,我该如何解决?(What is a NullReferenceException, and how do I fix it?)
【发布时间】:2022-01-26 00:32:30
【问题描述】:

我有一些代码,当它执行时,它会抛出一个 NullReferenceException ,说:

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

这是什么意思,我可以做些什么来解决这个错误?

  • VS 2017 中的异常帮助器将更有助于诊断此异常的原因 -- New Exception Helper 下的 blogs.msdn.microsoft.com/visualstudio/2016/11/28/…
  • 尊敬的未来访问者,此问题的答案同样适用于 ArgumentNullException 。如果您的问题已作为此问题的副本而关闭,并且您遇到了 ANE,请按照答案中的说明进行调试并解决您的问题。
  • @will ANE 只有在将 null 作为参数传递时才会发生。如果 ANE 问题与此问题重复,您能举个例子吗?
  • 它出现在 Meta 上,但我必须去挖掘链接。但至于该评论,ANE 只是一个 NRE,但有人添加了先发制人的检查,并且您至少确切知道什么是 null(提供了参数名称),因此它比直接 NRE 更容易诊断。< /跨度>

【解决方案1】:

是什么原因?

底线

您正在尝试使用 null (或 VB.NET 中的 Nothing )。这意味着您要么将其设置为 null ,要么根本不将其设置为任何值。

像其他任何东西一样, null 被传递。如果是 null in 方法“A”,则可能是方法“B”传递了 null to 方法“A”。

null 可以有不同的含义:

  1. 对象变量 未初始化 ,因此 指向任何内容。 在这种情况下,如果您访问此类对象的成员,则会导致 NullReferenceException
  2. 开发人员 故意使用 null 来表明没有可用的有意义的值。 请注意,C# 具有变量可空数据类型的概念(如数据库表可以具有可空字段) - 您可以将 null 分配给它们以指示其中没有存储任何值,例如 int? a = null; (这是 Nullable<int> a = null; 的快捷方式),其中问号表示允许将 null 存储在变量 a 中。您可以使用 if (a.HasValue) {...} if (a==null) {...} 进行检查。可空变量,如本例中的 a ,允许通过 a.Value 显式访问值,或者通过 a 正常访问。
    注意 如果 a null ,则通过 a.Value 访问它会抛出 InvalidOperationException 而不是 NullReferenceException - 你应该事先进行检查,即如果你有另一个不可为空的变量 int b; 那么你应该做像 if (a.HasValue) { b = a.Value; } 或更短的 if (a != null) { b = a; } 这样的赋值。

本文的其余部分更详细地介绍了许多程序员经常犯的可能导致 NullReferenceException 的错误。

更具体

runtime 抛出 NullReferenceException always 表示相同的意思:您正在尝试使用引用,但引用未初始化(或者它是 一次 已初始化,但 不再 已初始化)。

这意味着引用是 null ,您不能通过 null 引用访问成员(例如方法)。最简单的情况:

string foo = null;
foo.ToUpper();

这将在第二行抛出 NullReferenceException ,因为您不能在指向 null string 引用上调用实例方法 ToUpper()

调试

您如何找到 NullReferenceException 的来源?除了查看异常本身(将在其发生的位置准确抛出)之外,Visual Studio 中的一般调试规则适用:放置战略断点和 inspect your variables ,或者将鼠标悬停在它们的名称上,打开一个 ( Quick)Watch 窗口或使用各种调试面板,如 Locals 和 Autos。

如果您想找出引用设置或未设置的位置,请右键单击其名称并选择“查找所有引用”。然后,您可以在每个找到的位置放置一个断点,并在附加调试器的情况下运行您的程序。每次调试器在这样的断点处中断时,您都需要确定您是否期望引用为非空,检查变量,并验证它是否在您期望的时候指向一个实例。

按照这样的程序流程,你可以找到实例不应该为空的位置,以及为什么没有正确设置。

示例

可以抛出异常的一些常见场景:

通用

ref1.ref2.ref3.member

如果 ref1 或 ref2 或 ref3 为空,那么您将获得 NullReferenceException 。如果你想解决这个问题,那么通过将表达式重写为更简单的等价物来找出哪个是空的:

var r1 = ref1;
var r2 = r1.ref2;
var r3 = r2.ref3;
r3.member

具体来说,在 HttpContext.Current.User.Identity.Name 中, HttpContext.Current 可以为空,或者 User 属性可以为空,或者 Identity 属性可以为空。

间接

public class Person 
{
    public int Age { get; set; }
}
public class Book 
{
    public Person Author { get; set; }
}
public class Example 
{
    public void Foo() 
    {
        Book b1 = new Book();
        int authorAge = b1.Author.Age; // You never initialized the Author property.
                                       // there is no Person to get an Age from.
    }
}

如果要避免子 (Person) 空引用,可以在父 (Book) 对象的构造函数中对其进行初始化。

嵌套对象初始化器

这同样适用于嵌套对象初始化器:

Book b1 = new Book 
{ 
   Author = { Age = 45 } 
};

这转化为:

Book b1 = new Book();
b1.Author.Age = 45;

虽然使用了 new 关键字,但它只创建了 Book 的新实例,而不是 Person 的新实例,所以 Author 属性仍然是 null

嵌套集合初始化器

public class Person 
{
    public ICollection Books { get; set; }
}
public class Book 
{
    public string Title { get; set; }
}

嵌套集合 Initializers 行为相同:

Person p1 = new Person 
{
    Books = {
         new Book { Title = "Title1" },
         new Book { Title = "Title2" },
    }
};

这转化为:

Person p1 = new Person();
p1.Books.Add(new Book { Title = "Title1" });
p1.Books.Add(new Book { Title = "Title2" });

new Person 只创建了一个 Person 的实例,但 Books 集合仍然是 null 。集合 Initializer 语法不会创建集合 对于 p1.Books ,它只转换为 p1.Books.Add(...) 语句。

数组

int[] numbers = null;
int n = numbers[0]; // numbers is null. There is no array to index.

数组元素

Person[] people = new Person[5];
people[0].Age = 20 // people[0] is null. The array was allocated but not
                   // initialized. There is no Person to set the Age for.

锯齿状数组

long[][] array = new long[1][];
array[0][0] = 3; // is null because only the first dimension is yet initialized.
                 // Use array[0] = new long[2]; first.

集合/列表/字典

Dictionary agesForNames = null;
int age = agesForNames["Bob"]; // agesForNames is null.
                               // There is no Dictionary to perform the lookup.

范围变量(间接/延迟)

public class Person 
{
    public string Name { get; set; }
}
var people = new List();
people.Add(null);
var names = from p in people select p.Name;
string firstName = names.First(); // Exception is thrown here, but actually occurs
                                  // on the line above.  "p" is null because the
                                  // first element we added to the list is null.

事件 (C#)

public class Demo
{
    public event EventHandler StateChanged;
    
    protected virtual void OnStateChanged(EventArgs e)
    {        
        StateChanged(this, e); // Exception is thrown here 
                               // if no event handlers have been attached
                               // to StateChanged event
    }
}

(注意:VB.NET 编译器为事件使用插入空值检查,因此没有必要在 VB.NET 中检查 Nothing 的事件。)

错误的命名约定:

如果您对字段的命名与本地名称不同,您可能已经意识到您从未初始化过该字段。

public class Form1
{
    private Customer customer;
    
    private void Form1_Load(object sender, EventArgs e) 
    {
        Customer customer = new Customer();
        customer.Name = "John";
    }
    
    private void Button_Click(object sender, EventArgs e)
    {
        MessageBox.Show(customer.Name);
    }
}

这可以通过以下划线前缀字段的约定来解决:

    private Customer _customer;

ASP.NET 页面生命周期:

public partial class Issues_Edit : System.Web.UI.Page
{
    protected TestIssue myIssue;

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
             // Only called on first load, not when button clicked
             myIssue = new TestIssue(); 
        }
    }
        
    protected void SaveButton_Click(object sender, EventArgs e)
    {
        myIssue.Entry = "NullReferenceException here!";
    }
}

ASP.NET 会话值

// if the "FirstName" session value has not yet been set,
// then this line will throw a NullReferenceException
string firstName = Session["FirstName"].ToString();

ASP.NET MVC 空视图模型

如果在 ASP.NET MVC View 中引用 @Model 的属性时发生异常,您需要了解 Model 在您的操作方法中设置,当您 return 一个视图时。当您从控制器返回空模型(或模型属性)时,视图访问它时会发生异常:

// Controller
public class Restaurant:Controller
{
    public ActionResult Search()
    {
        return View();  // Forgot the provide a Model here.
    }
}

// Razor view 
@foreach (var restaurantSearch in Model.RestaurantSearch)  // Throws.
{
}
    

@Model.somePropertyName

WPF 控件创建顺序和事件

WPF 控件是在调用 InitializeComponent 期间按照它们在可视化树中出现的顺序创建的。 NullReferenceException 将在带有事件处理程序等的早期创建控件的情况下引发,在 InitializeComponent 期间触发,引用后期创建的控件。

例如:


    
    
       
       
       
    
        
    
    

这里 comboBox1 是在 label1 之前创建的。如果 comboBox1_SelectionChanged 尝试引用`label1,它还没有被创建。

private void comboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    label1.Content = comboBox1.SelectedIndex.ToString(); // NullReferenceException here!!
}

更改 XAML 中声明的顺序(即,在 comboBox1 之前列出 label1 ,忽略设计理念问题)至少可以解决 NullReferenceException 这里的问题。

使用 as 转换

var myThing = someObject as Thing;

这不会抛出 InvalidCastException ,而是在转换失败时返回 null (并且当 someObject 本身为空时)。所以请注意这一点。

LINQ FirstOrDefault() SingleOrDefault()

普通版本 First() Single() 在没有任何内容时抛出异常。在这种情况下,“OrDefault”版本返回 null 。所以请注意这一点。

foreach

foreach 在您尝试迭代 null 集合时抛出。通常是由返回集合的方法的意外 null 结果引起的。

List list = null;    
foreach(var v in list) { } // NullReferenceException here

更现实的例子 - 从 XML 文档中选择节点。如果未找到节点,但初始调试显示所有属性都有效时会抛出:

foreach (var node in myData.MyXml.DocumentNode.SelectNodes("//Data"))

避免方法

明确检查 null 并忽略 null 值。

如果您希望引用有时是 null ,您可以在访问实例成员之前检查它是否为 null

void PrintName(Person p)
{
    if (p != null) 
    {
        Console.WriteLine(p.Name);
    }
}

明确检查 null 并提供默认值。

您调用的期望实例的方法可以返回 null ,例如当找不到正在寻找的对象时。在这种情况下,您可以选择返回默认值:

string GetCategory(Book b) 
{
    if (b == null)
        return "Unknown";
    return b.Category;
}

从方法调用中显式检查 null 并抛出自定义异常。

你也可以抛出自定义异常,只在调用代码中捕获它:

string GetCategory(string bookTitle) 
{
    var book = library.FindBook(bookTitle);  // This may return null
    if (book == null)
        throw new BookNotFoundException(bookTitle);  // Your custom exception
    return book.Category;
}

如果值不应该是 null ,请使用 Debug.Assert ,以便在异常发生之前发现问题。

当您在开发过程中知道一个方法可以但绝不应该返回 null ,您可以在它确实发生时使用 Debug.Assert() 尽快中断:

string GetTitle(int knownBookID) 
{
    // You know this should never return null.
    var book = library.GetBook(knownBookID);  

    // Exception will occur on the next line instead of at the end of this method.
    Debug.Assert(book != null, "Library didn't return a book for known book ID.");

    // Some other code

    return book.Title; // Will never throw NullReferenceException in Debug mode.
}

虽然这个检查 will not end up in your release build ,导致它在运行时 book == null 在发布模式下再次抛出 NullReferenceException

nullable 值类型使用 GetValueOrDefault() 以在它们为 null 时提供默认值。

DateTime? appointment = null;
Console.WriteLine(appointment.GetValueOrDefault(DateTime.Now));
// Will display the default value provided (DateTime.Now), because appointment is null.

appointment = new DateTime(2022, 10, 20);
Console.WriteLine(appointment.GetValueOrDefault(DateTime.Now));
// Will display the appointment date, not the default

使用空合并运算符: ?? [C#] 或 If() [VB]。

遇到 null 时提供默认值的简写:

IService CreateService(ILogger log, Int32? frobPowerLevel)
{
   var serviceImpl = new MyService(log ?? NullLog.Instance);
 
   // Note that the above "GetValueOrDefault()" can also be rewritten to use
   // the coalesce operator:
   serviceImpl.FrobPowerLevel = frobPowerLevel ?? 5;
}

使用空条件运算符: ?. ?[x] 用于数组(在 C# 6 和 VB.NET 14 中可用):

这有时也称为安全导航或 Elvis(根据其形状)运算符。如果运算符左侧的表达式为 null,则不会计算右侧的表达式,而是返回 null。这意味着这样的情况:

var title = person.Title.ToUpper();

如果此人没有头衔,这将引发异常,因为它试图在具有空值的属性上调用 ToUpper

C# 5 及以下,可以使用:

var title = person.Title == null ? null : person.Title.ToUpper();

现在 title 变量将为 null 而不是抛出异常。 C# 6 为此引入了更短的语法:

var title = person.Title?.ToUpper();

这将导致标题变量为 null ,如果 person.Title null ,则不会调用 ToUpper

当然,您 仍然 必须检查 title 中的 null 或使用空条件运算符和空合并运算符 ( ?? ) 来提供默认值:

// regular null check
int titleLength = 0;
if (title != null)
    titleLength = title.Length; // If title is null, this would throw NullReferenceException
    
// combining the `?` and the `??` operator
int titleLength = title?.Length ?? 0;

同样,对于数组,您可以使用 ?[i] ,如下所示:

int[] myIntArray = null;
var i = 5;
int? elem = myIntArray?[i];
if (!elem.HasValue) Console.WriteLine("No value");

这将执行以下操作:如果 myIntArray null ,则表达式返回 null ,您可以安全地检查它。如果它包含一个数组,它将执行以下操作: elem = myIntArray[i]; 并返回第 i th 元素。

使用空上下文(在 C# 8 中可用):

C# 8 中引入,空上下文和可空引用类型对变量执行静态分析,并在值可能为 null 或已设置为 null 时提供编译器警告。可空引用类型允许明确允许类型为 null

可以使用 csproj 文件中的 Nullable 元素为项目设置可为空的注释上下文和可为空的警告上下文。此元素配置编译器如何解释类​​型的可空性以及生成哪些警告。有效设置为:

  • enable :可空注释上下文已启用。可空警告上下文已启用。例如,引用类型的变量(字符串)是不可为空的。所有可空性警告均已启用。
  • disable :可空注释上下文已禁用。可空警告上下文已禁用。引用类型的变量是无意识的,就像 C# 的早期版本一样。所有可空性警告均已禁用。
  • safeonly :可空注释上下文已启用。可为空的警告上下文是仅安全的。引用类型的变量是不可为空的。所有安全可空性警告均已启用。
  • warnings :可空注释上下文已禁用。可空警告上下文已启用。引用类型的变量是不经意的。所有可空性警告均已启用。
  • safeonlywarnings :可空注释上下文已禁用。可为空的警告上下文是仅安全的。 引用类型的变量是不经意的。所有安全可空性警告均已启用。

可空引用类型使用与可空值类型相同的语法来注明: ? 附加到变量的类型。

调试和修复迭代器中的 null derefs 的特殊技术

C# 支持“迭代器块”(在其他一些流行语言中称为“生成器”)。由于延迟执行, NullReferenceException 在迭代器块中调试可能特别棘手:

public IEnumerable GetFrobs(FrobFactory f, int count)
{
    for (int i = 0; i < count; ++i)
    yield return f.MakeFrob();
}
...
FrobFactory factory = whatever;
IEnumerable frobs = GetFrobs();
...
foreach(Frob frob in frobs) { ... }

如果 whatever 导致 null MakeFrob 将抛出。现在,您可能认为正确的做法是:

// DON'T DO THIS
public IEnumerable GetFrobs(FrobFactory f, int count)
{
   if (f == null) 
      throw new ArgumentNullException("f", "factory must not be null");
   for (int i = 0; i < count; ++i)
      yield return f.MakeFrob();
}

为什么会这样?因为迭代器块直到 foreach ! 才真正 运行 !对 GetFrobs 的调用只返回一个对象, 在迭代时 将运行迭代器块。

通过编写这样的 null 检查可以防止 NullReferenceException ,但是您将 NullArgumentException 移动到 迭代 点,而不是 调用点 ,这 调试起来非常混乱

正确的解决方法是:

// DO THIS
public IEnumerable GetFrobs(FrobFactory f, int count)
{
   // No yields in a public method that throws!
   if (f == null) 
       throw new ArgumentNullException("f", "factory must not be null");
   return GetFrobsForReal(f, count);
}
private IEnumerable GetFrobsForReal(FrobFactory f, int count)
{
   // Yields in a private method
   Debug.Assert(f != null);
   for (int i = 0; i < count; ++i)
        yield return f.MakeFrob();
}

也就是说,创建一个具有迭代器块逻辑的私有辅助方法和一个执行 null 检查并返回迭代器的公共表面方法。现在当 GetFrobs 被调用时, null 检查立即发生,然后 GetFrobsForReal 在序列迭代时执行。

如果您检查 LINQ 到 Objects 的引用源,您会发现该技术始终被使用。写起来有点笨拙,但它使调试无效错误更容易。 为了调用者的方便而不是作者的方便而优化你的代码

关于不安全代码中的 null 取消引用的说明

C# 有一个“不安全”模式,顾名思义,这是非常危险的,因为提供内存安全和类型安全的正常安全机制没有被强制执行。 除非您对内存的工作原理有透彻和深入的了解,否则不应编写不安全的代码

在不安全模式下,您应该注意两个重要事实:

  • 取消引用 null pointer 会产生与取消引用 null reference 相同的异常
  • 取消引用无效的非空指针 可以 在某些情况下产生该异常

要理解为什么会这样,首先要了解 .NET 如何产生 NullReferenceException 。 (这些细节适用于在 Windows 上运行的 .NET;其他操作系统使用类似的机制。)

内存在 Windows 中虚拟化;每个进程获得由操作系统跟踪的许多内存“页面”的虚拟内存空间。内存的每一页都设置了标志,这些标志决定了它可以如何使用:读取、写入、执行等等。 最低 页面被标记为“如果以任何方式使用都会产生错误”。

C# 中的空指针和空引用在内部都表示为数字零,因此任何将其取消引用到相应内存存储的尝试都会导致操作系统产生错误。 .NET 运行时检测到此错误并将其转换为 NullReferenceException

这就是解引用空指针和空引用会产生相同异常的原因。

第二点呢?取消引用 任何 位于虚拟内存最低页面的无效指针会导致相同的操作系统错误,从而导致相同的异常。

为什么这有意义?好吧,假设我们有一个包含两个 int 的结构和一个等于 null 的非托管指针。如果我们尝试取消引用结构中的第二个 int, CLR 将不会尝试访问位置 0 的存储;它将访问位置 4 的存储。但从逻辑上讲,这是一个 null 取消引用,因为我们通过 null 到达那个地址。

如果您正在使用不安全的代码并且收到 NullReferenceException ,请注意违规指针不必为空。可以是最低页面的任意位置,都会产生这个异常。

  • 也许这是一个愚蠢的评论,但避免这个问题的第一个也是最好的方法不是初始化对象吗?对我来说,如果发生此错误,通常是因为我忘记初始化数组元素之类的东西。我认为将对象定义为 null 然后引用它的情况要少得多。也许给出解决与描述相邻的每个问题的方法。仍然是个好帖子。
  • 如果没有对象,而是方法或属性的返回值怎么办?
  • 书/作者的例子有点奇怪....那怎么编译?智能感知是如何工作的?这是什么我不擅长电脑...
  • @Will:我上次的编辑有帮助吗?如果不是,那么请更明确地说明您认为是什么问题。
  • @JohnSaunders 哦,不,抱歉,我的意思是对象初始化器版本。 new Book { Author = { Age = 45 } }; 内部初始化如何......我想不出内部初始化会工作的情况,但它可以编译并且智能感知工作......除非结构?