一尘不染

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

javascript

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

你调用的对象是空的。

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


阅读 168

收藏
2022-02-15

共1个答案

一尘不染

底线

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

像其他任何事情一样,null被传递。如果它null 方法“A”中,则可能是方法“B”将 a 传递给null 方法“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.Valuethrows anInvalidOperationException而不是NullReferenceExceptionif访问anull- 你应该事先进行检查,即如果你有另一个不可为空的变量int b;,那么你应该做类似if (a.HasValue) { b = a.Value; }或更短的赋值if (a != null) { b = a; }

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

进一步来说

runtime抛出 aNullReferenceException 总是意味着同样的事情:你试图使用一个引用,并且引用没有被初始化(或者它曾经被初始化,但不再被初始化)。

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

string foo = null;
foo.ToUpper();

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

调试

如何找到 a 的来源NullReferenceException?除了查看异常本身(将在其发生的位置准确抛出)之外,Visual Studio 中的一般调试规则也适用:放置战略断点并检查您的变量,方法是将鼠标悬停在它们的名称上,打开一个 ( 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.NameHttpContext.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<Book> 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只创建了 的实例,PersonBooks集合仍然是null。集合Initializer语法不会为 创建集合p1.Books,它只转换为p1.Books.Add(...)语句。

Array

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

Array Elements

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.

Jagged Arrays

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.

Collection/List/Dictionary

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

Range Variable (Indirect/Deferred)

public class Person 
{
    public string Name { get; set; }
}
var people = new List<Person>();
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.

Events (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 编译器为事件使用插入空值检查,因此没有必要Nothing在 VB.NET 中检查事件。)

错误的命名约定:

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

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 空视图模型

如果在引用 in 的属性时发生异常@ModelASP.NET MVC View您需要了解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.
{
}

<p>@Model.somePropertyName</p> <!-- Also throws -->

WPF 控件创建顺序和事件

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

例如:

<Grid>
    <!-- Combobox declared first -->
    <ComboBox Name="comboBox1" 
              Margin="10"
              SelectedIndex="0" 
              SelectionChanged="comboBox1_SelectionChanged">
       <ComboBoxItem Content="Item 1" />
       <ComboBoxItem Content="Item 2" />
       <ComboBoxItem Content="Item 3" />
    </ComboBox>

    <!-- Label declared later -->
    <Label Name="label1" 
           Content="Label"
           Margin="10" />
</Grid>

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

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

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

as

var myThing = someObject as Thing;

这不会抛出 anInvalidCastException但在强制转换失败时返回 a null(并且当someObject它本身为 null 时)。所以请注意这一点。

LINQFirstOrDefault()SingleOrDefault()

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

前锋

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

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

使用Debug.Assertif value should never be null,在异常发生之前捕获问题。

当您在开发过程中知道一个方法可以但永远不应该返回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.
}

尽管此检查不会在您的发布版本中结束,但会导致它在运行时处于发布模式时NullReferenceException再次抛出。book == null

用于值类型以在它们为 时提供默认GetValueOrDefault()值。nullable``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]。

遇到a 时提供默认值的简写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;
}

使用 null 条件运算符:?.?[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();

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

当然,您仍然需要检查titlenull使用 null 条件运算符和 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");

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

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

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

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

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

可空引用类型使用与可空值类型相同的语法来记录:将 a?附加到变量的类型。

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

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

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

如果whatever结果在nullthenMakeFrob将抛出。现在,您可能认为正确的做法是:

// DON'T DO THIS
public IEnumerable<Frob> 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<Frob> 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<Frob> 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在序列迭代时执行。

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

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

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

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

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

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

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

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

这就是为什么同时取消引用空指针和空引用会产生相同的异常。

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

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

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

2022-02-15