什么是 NullReferenceException,如何解决?

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

你调用的对象是空的。

这是什么意思,我该怎么做才能解决此错误?

答案

原因是什么?

底线

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

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

null可以具有不同的含义:

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

本文的其余部分将更详细地说明,并说明许多程序员经常犯的错误,这些错误可能导致NullReferenceException

进一步来说

运行时抛出NullReferenceException 总是意味着同一件事:您正在尝试使用引用,并且该引用未初始化(或者曾经被初始化,但是不再被初始化)。

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

string foo = null;
foo.ToUpper();

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

调试

您如何找到NullReferenceException的来源?除了查看将要引发的异常本身之外,Visual Studio 中的一般调试规则也适用:放置战略断点并检查变量 ,方法是将鼠标悬停在它们的名称上,然后打开(快速)监视窗口或使用各种调试面板(例如本地和自动)。

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

通过以这种方式遵循程序流程,您可以找到实例不应为 null 的位置以及未正确设置实例的原因。

例子

可能引发异常的一些常见方案:

泛型

ref1.ref2.ref3.member

如果 ref1 或 ref2 或 ref3 为 null,则将获得NullReferenceException 。如果要解决此问题,请通过将表达式重写为更简单的等价项来找出哪个为空:

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

具体来说,在HttpContext.Current.User.Identity.NameHttpContext.Current可以为 null,或者User属性可以为 null,或者Identity属性可以为 null。

间接

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

嵌套的集合初始值设定项的行为相同:

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 。集合初始值设定项语法不会为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<string, int> 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<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.

大事记

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

错误的命名约定:

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

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 视图中引用@Model的属性时发生异常,则需要了解在return视图时在操作方法中设置了Model 。当从控制器返回空模型(或模型属性)时,视图访问它时会发生异常:

// 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 ,该控件在引用晚创建控件的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>

在这里, comboBox1label1之前创建。如果comboBox1_SelectionChanged尝试引用 `label1,则尚未创建。

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

更改 XAML 中的声明顺序(即,在comboBox1之前列出label1 ,而忽略了设计哲学的问题)至少会在这里解决NullReferenceException

投放as

var myThing = someObject as Thing;

这并不抛出一个 InvalidCastException 但返回null当转换失败(当 someObject 本身就是 NULL)。因此请注意。

LINQ FirstOrDefault()和 SingleOrDefault()

没有任何内容时,普通版本的First()Single()引发异常。在这种情况下,“OrDefault” 版本返回 null。因此请注意。

前言

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

List<int> list = null;    
 foreach(var v in list) { } // exception

更实际的示例 - 从 XML 文档中选择节点。如果未找到节点,但初始调试显示所有属性均有效,则会抛出该异常:

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

避免方法

明确检查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.
}

尽管此检查不会在您的发行版本中结束,导致在发行模式下运行时book == null时再次引发NullReferenceException

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();

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

当然,您仍然必须检查title是否为 null 或将 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");

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

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

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

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

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

一个:可空引用类型是使用相同的语法空值类型注意到?附加到变量的类型。

用于调试和修复迭代器中的空 deref 的特殊技术

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

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结果为nullMakeFrob将抛出。现在,您可能会认为正确的做法是:

// 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 检查,可以防止 null 取消引用,但是可以将 null 参数异常移至迭代点,而不是调用点,这会给调试带来很大的混乱

正确的解决方法是:

// 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();
}

也就是说,制作一个具有迭代器块逻辑的私有帮助器方法,以及一个进行空检查并返回迭代器的公共表面方法。现在, GetFrobs时,将立即执行空检查,然后在迭代序列时执行GetFrobsForReal

如果检查 LINQ to Objects 的参考源,您会发现该技术始终被使用。编写起来有点笨拙,但是它使调试 null 错误更加容易。 优化代码以方便调用者,而不是作者

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

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

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

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

要了解为什么会这样,它有助于首先了解. NET 如何产生 null 取消引用异常。 (这些详细信息适用于 Windows 上运行的. NET;其他操作系统使用类似的机制。)

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

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

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

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

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

如果您使用的是不安全的代码,并且会收到 null 解除引用异常,则请注意,有问题的指针不必为 null。它可以在最低页面的任何位置,并且将产生此异常。

NullReference 异常— Visual Basic

Visual BasicNullReference ExceptionC#中的 NullReference Exception相同。毕竟,它们都报告了它们都使用的. NET Framework 中定义的相同异常。 Visual Basic 特有的原因很少(也许只有一个)。

该答案将使用 Visual Basic 术语,语法和上下文。使用的示例来自大量过去的 Stack Overflow 问题。这是通过使用帖子中经常出现的各种情况来最大化相关性。还为可能需要的人提供了更多解释。此处可能列出与您类似的示例。

注意:

  1. 这是基于概念的:没有代码可粘贴到项目中。它旨在帮助您了解导致NullReferenceException (NRE)的原因,如何找到它,如何修复它以及如何避免它。 NRE 可以通过多种方式引起,因此这不可能是您唯一遇到的问题。
  2. 这些示例(来自 Stack Overflow 的帖子)并不总是始终显示最佳的方法。
  3. 通常,使用最简单的补救措施。

基本意义

消息“对象未设置为对象的实例”表示您正在尝试使用尚未初始化的对象。归结为以下之一:

  • 您的代码声明了一个对象变量,但是没有初始化它(创建实例或 “ 实例化 ” 它)
  • 您的代码假定的某些操作会初始化一个对象,但不会
  • 可能是其他代码过早地使仍在使用的对象无效

找出原因

由于问题是一个对象引用,为Nothing ,答案是检查它们以找出哪个对象。然后确定为什么不初始化。将鼠标悬停在各个变量上,Visual Studio(VS)将显示其值 - 罪魁祸首是Nothing

IDE调试显示

您还应该从相关代码中删除任何 Try / Catch 块,尤其是在 Catch 块中没有任何内容的地方。当它尝试使用Nothing对象时,这将导致您的代码崩溃。 这就是您想要的,因为它将识别出问题的确切位置 ,并允许您识别导致问题的对象。

Catch 中的一个MsgBoxError while...时显示Error while...将无济于事。这种方法还会导致非常糟糕的堆栈溢出问题,因为您无法描述实际的异常,所涉及的对象甚至发生异常的代码行。

您还可以使用Locals WindowDebug-> Locals Window > Locals )检查对象。

一旦知道了问题所在和出处,通常比发布新问题要容易得多,而且速度也更快。

也可以看看:

实例和补救措施

类对象 / 创建实例

Dim reg As CashRegister
...
TextBox1.Text = reg.Amount         ' NRE

问题是Dim不会创建 CashRegister 对象 。它仅声明该类型的名为reg的变量。 声明对象变量和创建实例是两件事。

补救

在声明实例时,通常可以使用New运算符创建实例:

Dim reg As New CashRegister        ' [New] creates instance, invokes the constructor

' Longer, more explicit form:
Dim reg As CashRegister = New CashRegister

仅在以后创建实例时:

Private reg As CashRegister         ' Declare
  ...
reg = New CashRegister()            ' Create instance

注意: 不要在过程中再次使用Dim ,包括构造函数( Sub New ):

Private reg As CashRegister
'...

Public Sub New()
   '...
   Dim reg As New CashRegister
End Sub

这将创建一个局部变量reg ,该变量仅在该上下文(子)中存在。该reg与模块级的可变Scope ,您将使用在其他地方仍然Nothing

缺少New运算符是导致NullReference Exceptions的#1 原因,在所检查的堆栈溢出问题中可见。

Visual Basic 尝试使用New反复使过程清晰:使用New Operator 创建一个对象并调用Sub New (构造函数),您的对象可以在其中执行任何其他初始化。

需要明确的是, Dim (或Private )仅声明一个变量及其Type 。变量的作用域 - 是否在整个模块 / 类中存在,还是过程的局部变量 - 由声明位置确定。 Private | Friend | Public定义访问级别,而不是Scope

有关更多信息,请参见:


数组

数组也必须实例化:

Private arr as String()

仅声明了此数组,未创建。有几种初始化数组的方法:

Private arr as String() = New String(10){}
' or
Private arr() As String = New String(10){}

' For a local array (in a procedure) and using 'Option Infer':
Dim arr = New String(10) {}

注意:从 VS 2010 开始,使用文字和Option Infer初始化本地数组时, As <Type>New元素是可选的:

Dim myDbl As Double() = {1.5, 2, 9.9, 18, 3.14}
Dim myDbl = New Double() {1.5, 2, 9.9, 18, 3.14}
Dim myDbl() = {1.5, 2, 9.9, 18, 3.14}

数据类型和数组大小是从分配的数据推断出来的。类 / 模块级别的声明仍然需要带有Option Strict As <Type>

Private myDoubles As Double() = {1.5, 2, 9.9, 18, 3.14}

示例:类对象数组

Dim arrFoo(5) As Foo

For i As Integer = 0 To arrFoo.Count - 1
   arrFoo(i).Bar = i * 10       ' Exception
Next

数组已创建,但其中的Foo对象尚未创建。

补救

For i As Integer = 0 To arrFoo.Count - 1
    arrFoo(i) = New Foo()         ' Create Foo instance
    arrFoo(i).Bar = i * 10
Next

使用List(Of T)会使没有有效对象的元素变得非常困难:

Dim FooList As New List(Of Foo)     ' List created, but it is empty
Dim f As Foo                        ' Temporary variable for the loop

For i As Integer = 0 To 5
    f = New Foo()                    ' Foo instance created
    f.Bar =  i * 10
    FooList.Add(f)                   ' Foo object added to list
Next

有关更多信息,请参见:


清单和收藏

.NET 集合(种类繁多 - 列表,字典等)也必须实例化或创建。

Private myList As List(Of String)
..
myList.Add("ziggy")           ' NullReference

由于相同的原因,您将获得相同的异常 - 仅声明了myList ,但未创建实例。补救措施是相同的:

myList = New List(Of String)

' Or create an instance when declared:
Private myList As New List(Of String)

常见的疏忽是使用集合Type

Public Class Foo
    Private barList As List(Of Bar)

    Friend Function BarCount As Integer
        Return barList.Count
    End Function

    Friend Sub AddItem(newBar As Bar)
        If barList.Contains(newBar) = False Then
            barList.Add(newBar)
        End If
    End Function

这两个过程都将导致 NRE,因为只声明了barList ,而不实例化了barList 。创建Foo的实例也不会创建内部barList的实例。可能是打算在构造函数中执行此操作:

Public Sub New         ' Constructor
    ' Stuff to do when a new Foo is created...
    barList = New List(Of Bar)
End Sub

和以前一样,这是不正确的:

Public Sub New()
    ' Creates another barList local to this procedure
     Dim barList As New List(Of Bar)
End Sub

有关更多信息,请参见List(Of T)


数据提供者对象

使用数据库为 NullReference 提供了很多机会,因为可以同时使用许多对象( CommandConnectionTransactionDatasetDataTableDataRows ....)。 注意:使用哪个数据提供程序(MySQL,SQL Server,OleDB 等)无关紧要, 概念是相同的。

例子 1

Dim da As OleDbDataAdapter
Dim ds As DataSet
Dim MaxRows As Integer

con.Open()
Dim sql = "SELECT * FROM tblfoobar_List"
da = New OleDbDataAdapter(sql, con)
da.Fill(ds, "foobar")
con.Close()

MaxRows = ds.Tables("foobar").Rows.Count      ' Error

和以前一样,声明了ds Dataset 对象,但是从未创建实例。 DataAdapter将填充现有的DataSet ,而不创建一个。在这种情况下,由于ds是局部变量, 因此 IDE 警告您可能会发生这种情况:

img

当声明为模块 / 类级别的变量(如con的情况)时,编译器无法知道该对象是否由上游过程创建。不要忽略警告。

补救

Dim ds As New DataSet

例子 2

ds = New DataSet
da = New OleDBDataAdapter(sql, con)
da.Fill(ds, "Employees")

txtID.Text = ds.Tables("Employee").Rows(0).Item(1)
txtID.Name = ds.Tables("Employee").Rows(0).Item(2)

错字在这里是一个问题: Employees vs Employee 。没有创建名为 “Employee” 的DataTable ,因此会导致NullReferenceException尝试访问它。另一个潜在的问题是假设会有Items可能不会那么当 SQL 包括一个 WHERE 子句。

补救

由于它使用一个表,因此使用Tables(0)可以避免拼写错误。检查Rows.Count还可以帮助:

If ds.Tables(0).Rows.Count > 0 Then
    txtID.Text = ds.Tables(0).Rows(0).Item(1)
    txtID.Name = ds.Tables(0).Rows(0).Item(2)
End If

Fill是一个返回受影响的Rows数的函数,也可以对其进行测试:

If da.Fill(ds, "Employees") > 0 Then...

例子 3

Dim da As New OleDb.OleDbDataAdapter("SELECT TICKET.TICKET_NO,
        TICKET.CUSTOMER_ID, ... FROM TICKET_RESERVATION AS TICKET INNER JOIN
        FLIGHT_DETAILS AS FLIGHT ... WHERE [TICKET.TICKET_NO]= ...", con)
Dim ds As New DataSet
da.Fill(ds)

If ds.Tables("TICKET_RESERVATION").Rows.Count > 0 Then

DataAdapter将提供上一个示例中所示的TableNames ,但不会解析 SQL 或数据库表中的名称。结果, ds.Tables("TICKET_RESERVATION")引用了不存在的表。

补救措施是相同的,按索引引用表:

If ds.Tables(0).Rows.Count > 0 Then

另请参见DataTable 类


对象路径 / 嵌套

If myFoo.Bar.Items IsNot Nothing Then
   ...

该代码仅用于测试ItemsmyFooBar可能为 Nothing。 补救措施是一次测试一个对象的整个链或路径:

If (myFoo IsNot Nothing) AndAlso
    (myFoo.Bar IsNot Nothing) AndAlso
    (myFoo.Bar.Items IsNot Nothing) Then
    ....

AndAlso也很重要。一旦遇到第一个False条件,将不执行后续测试。这允许代码一次安全地 “钻” 入对象一个 “级别”,仅在(如果确定) myFoo有效之后评估myFoo.Bar 。编码复杂对象时,对象链或路径可能会变得很长:

myBase.myNodes(3).Layer.SubLayer.Foo.Files.Add("somefilename")

无法引用null对象的任何 “下游” 对象。这也适用于控件:

myWebBrowser.Document.GetElementById("formfld1").InnerText = "some value"

在这里, myWebBrowserDocument可能为 Nothing 或formfld1元素可能不存在。


UI 控件

Dim cmd5 As New SqlCommand("select Cartons, Pieces, Foobar " _
     & "FROM Invoice where invoice_no = '" & _
     Me.ComboBox5.SelectedItem.ToString.Trim & "' And category = '" & _
     Me.ListBox1.SelectedItem.ToString.Trim & "' And item_name = '" & _
     Me.ComboBox2.SelectedValue.ToString.Trim & "' And expiry_date = '" & _
     Me.expiry.Text & "'", con)

除其他外,此代码无法预期用户可能未在一个或多个 UI 控件中选择某些内容。 ListBox1.SelectedItem可能为Nothing ,因此ListBox1.SelectedItem.ToString将导致 NRE。

补救

在使用前验证数据(也使用Option Strict和 SQL 参数):

Dim expiry As DateTime         ' for text date validation
If (ComboBox5.SelectedItems.Count > 0) AndAlso
    (ListBox1.SelectedItems.Count > 0) AndAlso
    (ComboBox2.SelectedItems.Count > 0) AndAlso
    (DateTime.TryParse(expiry.Text, expiry) Then

    '... do stuff
Else
    MessageBox.Show(...error message...)
End If

或者,您可以使用(ComboBox5.SelectedItem IsNot Nothing) AndAlso...


Visual Basic 表单

Public Class Form1

    Private NameBoxes = New TextBox(5) {Controls("TextBox1"), _
                   Controls("TextBox2"), Controls("TextBox3"), _
                   Controls("TextBox4"), Controls("TextBox5"), _
                   Controls("TextBox6")}

    ' same thing in a different format:
    Private boxList As New List(Of TextBox) From {TextBox1, TextBox2, TextBox3 ...}

    ' Immediate NRE:
    Private somevar As String = Me.Controls("TextBox1").Text

这是获得 NRE 的相当普遍的方法。在 C#中,根据其编码方式,IDE 将报告Controls在当前上下文中不存在,或者 “无法引用非静态成员”。因此,在某种程度上,这是仅 VB 的情况。这也很复杂,因为它可能导致失败的级联。

数组和集合不能以这种方式初始化。此初始化代码将构造函数创建FormControls 之前运行。结果是:

  • 列表和集合将为空
  • 数组将包含五个 Nothing 元素
  • somevar分配将立即产生 NRE,因为 Nothing 没有.Text属性

稍后引用数组元素将导致 NRE。如果在Form_Load执行此操作,则由于一个奇怪的错误,IDE 可能不会在发生异常时报告该异常。当您的代码尝试使用该数组时,该异常将在以后弹出。 这篇文章详细介绍了这种 “无声例外”。出于我们的目的,关键是当创建表单时发生灾难性事件( Sub NewForm Load事件)时,可能不会报告异常,代码将退出过程并仅显示表单。

由于 NRE 之后将不会再运行Sub NewForm Load事件中的其他代码,因此许多其他事情可以保留为未初始化。

Sub Form_Load(..._
   '...
   Dim name As String = NameBoxes(2).Text        ' NRE
   ' ...
   ' More code (which will likely not be executed)
   ' ...
End Sub

请注意,这适用于所有和所有控件和组件引用,这些引用使它们在以下位置非法:

Public Class Form1

    Private myFiles() As String = Me.OpenFileDialog1.FileName & ...
    Private dbcon As String = OpenFileDialog1.FileName & ";Jet Oledb..."
    Private studentName As String = TextBox13.Text

部分补救

奇怪的是,VB 没有提供警告,但补救措施是在窗体级别声明容器,但在控件确实存在时在窗体加载事件处理程序中初始化它们。只要您的代码在InitializeComponent调用之后,就可以在Sub New完成:

' Module level declaration
Private NameBoxes as TextBox()
Private studentName As String

' Form Load, Form Shown or Sub New:
'
' Using the OP's approach (illegal using OPTION STRICT)
NameBoxes = New TextBox() {Me.Controls("TextBox1"), Me.Controls("TestBox2"), ...)
studentName = TextBox32.Text           ' For simple control references

数组代码可能还没有走出困境。 Me.Controls找不到容器控件中的任何控件(如GroupBoxPanel )。它们将位于该 Panel 或 GroupBox 的 Controls 集合中。拼写错误的控件名称( "TeStBox2" )也不会返回控件。在这种情况下, Nothing也不会再次存储在这些数组元素中,并且在您尝试引用它时会产生 NRE。

现在,您知道要查找的内容时,应该很容易找到它们: VS向您展示您的方式错误

“Button2” 位于Panel

补救

而不是使用表单的Controls集合按名称进行间接引用,而应使用控件引用:

' Declaration
Private NameBoxes As TextBox()

' Initialization -  simple and easy to read, hard to botch:
NameBoxes = New TextBox() {TextBox1, TextBox2, ...)

' Initialize a List
NamesList = New List(Of TextBox)({TextBox1, TextBox2, TextBox3...})
' or
NamesList = New List(Of TextBox)
NamesList.AddRange({TextBox1, TextBox2, TextBox3...})

函数什么都不返回

Private bars As New List(Of Bars)        ' Declared and created

Public Function BarList() As List(Of Bars)
    bars.Clear
    If someCondition Then
        For n As Integer = 0 to someValue
            bars.Add(GetBar(n))
        Next n
    Else
        Exit Function
    End If

    Return bars
End Function

在这种情况下,IDE 会警告您 “ 并非所有路径都返回值,并且可能会导致NullReferenceException ”。您可以通过将Exit Function替换为Return Nothing来抑制警告,但这不能解决问题。当someCondition = False时,任何尝试使用返回值的someCondition = False都将导致 NRE:

bList = myFoo.BarList()
For Each b As Bar in bList      ' EXCEPTION
      ...

补救

Return bList替换Exit Function中的Exit Function 。返回 List与返回Nothing 。如果返回的对象可能是Nothing ,请在使用它之前进行测试:

bList = myFoo.BarList()
 If bList IsNot Nothing Then...

实施不佳的尝试 / 捕获

实施不当的 “尝试 / 捕获”(Try / Catch)可能会隐藏问题所在并导致新问题:

Dim dr As SqlDataReader
Try
    Dim lnk As LinkButton = TryCast(sender, LinkButton)
    Dim gr As GridViewRow = DirectCast(lnk.NamingContainer, GridViewRow)
    Dim eid As String = GridView1.DataKeys(gr.RowIndex).Value.ToString()
    ViewState("username") = eid
    sqlQry = "select FirstName, Surname, DepartmentName, ExtensionName, jobTitle,
             Pager, mailaddress, from employees1 where username='" & eid & "'"
    If connection.State <> ConnectionState.Open Then
        connection.Open()
    End If
    command = New SqlCommand(sqlQry, connection)

    'More code fooing and barring

    dr = command.ExecuteReader()
    If dr.Read() Then
        lblFirstName.Text = Convert.ToString(dr("FirstName"))
        ...
    End If
    mpe.Show()
Catch

Finally
    command.Dispose()
    dr.Close()             ' <-- NRE
    connection.Close()
End Try

这是一种未按预期创建对象的情况,但也证明了空Catch的反作用。

SQL 中有一个多余的逗号(在 “mailaddress” 之后),导致.ExecuteReader处出现异常。在Catch不执行任何操作之后, Finally尝试执行清除操作,但是由于您无法Close null 的DataReader对象,因此会产生全新的NullReferenceException

一个空的Catch块是魔鬼的游乐场。这 OP 是百思不得其解,为什么他在得到一个 NRE Finally块。在其他情况下,空的Catch可能会导致更进一步的下游混乱,并导致您花费时间在错误的地方寻找错误的地方以解决问题。 (上述 “静默异常” 提供相同的娱乐价值。)

补救

不要使用空的 Try / Catch 块 - 让代码崩溃,以便您可以 a)查明原因 b)查明位置并 c)采取适当的补救措施。 Try / Catch 块无意于向唯一有资格修复它们的人员(开发人员)隐藏异常。


DBNull 与 Nothing 不同

For Each row As DataGridViewRow In dgvPlanning.Rows
    If Not IsDBNull(row.Cells(0).Value) Then
        ...

IsDBNull函数用于测试值是否等于System.DBNull来自 MSDN:

System.DBNull 值指示该 Object 表示缺少或不存在的数据。 DBNull 与 Nothing 不同,这表示变量尚未初始化。

补救

If row.Cells(0) IsNot Nothing Then ...

和以前一样,您可以先测试 Nothing,然后再测试特定值:

If (row.Cells(0) IsNot Nothing) AndAlso (IsDBNull(row.Cells(0).Value) = False) Then

例子 2

Dim getFoo = (From f In dbContext.FooBars
               Where f.something = something
               Select f).FirstOrDefault

If Not IsDBNull(getFoo) Then
    If IsDBNull(getFoo.user_id) Then
        txtFirst.Text = getFoo.first_name
    Else
       ...

FirstOrDefault返回第一项或默认值,对于引用类型为Nothing ,从不为DBNull

If getFoo IsNot Nothing Then...

控制项

Dim chk As CheckBox

chk = CType(Me.Controls(chkName), CheckBox)
If chk.Checked Then
    Return chk
End If

如果找不到(或存在于GroupBox )带有chkNameCheckBox ,则chk将为 Nothing,并且尝试引用任何属性都将导致异常。

补救

If (chk IsNot Nothing) AndAlso (chk.Checked) Then ...

DataGridView

DGV 定期出现一些怪癖:

dgvBooks.DataSource = loan.Books
dgvBooks.Columns("ISBN").Visible = True       ' NullReferenceException
dgvBooks.Columns("Title").DefaultCellStyle.Format = "C"
dgvBooks.Columns("Author").DefaultCellStyle.Format = "C"
dgvBooks.Columns("Price").DefaultCellStyle.Format = "C"

如果dgvBooks具有AutoGenerateColumns = True ,它将创建列,但未命名它们,因此当按名称引用它们时,上面的代码将失败。

补救

手动命名列,或通过索引引用:

dgvBooks.Columns(0).Visible = True

示例 2 —提防 NewRow

xlWorkSheet = xlWorkBook.Sheets("sheet1")

For i = 0 To myDGV.RowCount - 1
    For j = 0 To myDGV.ColumnCount - 1
        For k As Integer = 1 To myDGV.Columns.Count
            xlWorkSheet.Cells(1, k) = myDGV.Columns(k - 1).HeaderText
            xlWorkSheet.Cells(i + 2, j + 1) = myDGV(j, i).Value.ToString()
        Next
    Next
Next

当您的DataGridView AllowUserToAddRowsTrue (默认值)时,底部空白 / 新行中的Cells将全部包含Nothing 。使用内容的大多数尝试(例如ToString )将导致 NRE。

补救

使用For/Each循环并测试IsNewRow属性以确定它是否是最后一行。无论AllowUserToAddRows是否为 true,此方法都有效:

For Each r As DataGridViewRow in myDGV.Rows
    If r.IsNewRow = False Then
         ' ok to use this row

如果确实使用For n循环,则在IsNewRow为 true 时修改行数或使用Exit For


My.Settings(StringCollection)

在某些情况下,尝试使用My.Settings中的一个StringCollection可能会在您首次使用它时导致 NullReference。解决方案是相同的,但不是很明显。考虑:

My.Settings.FooBars.Add("ziggy")         ' foobars is a string collection

由于 VB 正在为您管理 “设置”,因此可以预期它会初始化集合。它将,但前提是您之前已在集合中添加了初始条目(在 “设置” 编辑器中)。因为收藏品都是(显然)在添加项目初始化,但它仍然Nothing当有设置没有项目编辑器来添加。

补救

如有必要,请在表单的Load事件处理程序中初始化设置集合:

If My.Settings.FooBars Is Nothing Then
    My.Settings.FooBars = New System.Collections.Specialized.StringCollection
End If

通常,仅在应用程序第一次运行时才需要初始化Settings集合。另一种解决方法是在 “ 项目”->“设置” |“设置” 中为您的集合添加初始值FooBars ,保存项目,然后删除假值。


关键点

您可能忘记了New运算符。

要么

您认为可以完美执行的操作可以将已初始化的对象返回到您的代码中,而事实并非如此。

永远不要忽略编译器警告,而要始终使用Option Strict On


MSDN NullReference 异常

另一种情况是将空对象转换为值类型时 。例如,下面的代码:

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

它将在演员表上抛出NullReferenceException 。在上面的示例中,这似乎很明显,但是这可能发生在更多的 “后期绑定” 复杂场景中,其中空对象是从您不拥有的某些代码中返回的,而强制转换例如是由某些自动系统生成的。

一个示例就是带有 Calendar 控件的简单 ASP.NET 绑定片段:

<asp:Calendar runat="server" SelectedDate="<%#Bind("Something")%>" />

在这里, SelectedDate实际上是Calendar Web Control 类型的DateTime类型的属性,并且绑定可以完美地返回 null。隐式 ASP.NET 生成器将创建一段与上面的强制转换代码等效的代码。这将引发一个NullReferenceException ,这很难发现,因为它位于 ASP.NET 生成的代码中,可以很好地编译...

这意味着所讨论的变量没有指向任何对象。我可以这样生成:

SqlConnection connection = null;
connection.Open();

这将引发错误,因为当我声明变量 “ connection ” 时,它没有指向任何东西。当我尝试将成员称为 “ Open ” 时,没有可解决的参考,它将引发错误。

为避免此错误:

  1. 尝试对它们执行任何操作之前,请务必对其进行初始化。
  2. 如果不确定对象是否为 null,请使用object == null检查。

JetBrains 的 Resharper 工具将识别代码中可能存在空引用错误的每个位置,从而使您可以进行空检查。此错误是错误的第一来源,恕我直言。

这意味着您的代码使用了设置为 null 的对象引用变量(即,它没有引用实际的对象实例)。

为避免该错误,可能为 null 的对象在使用前应进行 null 测试。

if (myvar != null)
{
    // Go ahead and use myvar
    myvar.property = ...
}
else
{
    // Whoops! myvar is null and cannot be used without first
    // assigning it to an instance reference
    // Attempting to use myvar here will result in NullReferenceException
}

请注意,无论哪种情况,.NET 中的原因始终相同:

您正在尝试使用值为Nothing / null的引用变量。当引用变量的值为Nothing / null ,这意味着它实际上并不持有对堆上存在的任何对象的实例的引用。

您要么从未为变量分配任何东西,从未创建分配给该变量的值的实例,要么手动将变量设置为Nothing / null ,或者调用了一个为您将变量设置为Nothing / null的函数。

抛出此异常的一个示例是:当您尝试检查某些内容时,该值为 null。

例如:

string testString = null; //Because it doesn't have a value (i.e. it's null; "Length" cannot do what it needs to do)

if (testString.Length == 0) // Throws a nullreferenceexception
{
    //Do something
}

当您尝试对尚未实例化的内容(即上面的代码)执行操作时,.NET 运行时将抛出 NullReferenceException。

与 ArgumentNullException 相比,如果方法期望传递给它的内容不为 null,则通常将其作为防御措施。

有关更多信息,请参见C#NullReferenceException 和 Null 参数

更新 C#8.0,2019:可空引用类型

C#8.0 引入了可为空的引用类型不可为空的引用类型 。因此,仅必须检查可为空的引用类型,以避免NullReferenceException


如果尚未初始化引用类型,并且要设置或读取其属性之一,则它将抛出NullReferenceException

例:

Person p = null;
p.Name = "Harry"; // NullReferenceException occurs here.

您可以通过检查变量是否不为空来简单地避免这种情况:

Person p = null;
if (p!=null)
{
    p.Name = "Harry"; // Not going to run to this point
}

要完全理解为什么会引发 NullReferenceException,了解值类型和 [引用类型] [3] 之间的区别非常重要。

因此,如果要处理值类型 ,则不会发生 NullReferenceExceptions。尽管在处理引用类型时需要保持警惕!

顾名思义,只有引用类型才能保存引用或从字面上指向任何内容(或 “空”)。而值类型始终包含一个值。

引用类型(必须检查这些类型):

  • 动态
  • 宾语

值类型(您可以简单地忽略这些类型):

  • 数值类型
  • 整体类型
  • 浮点类型
  • 小数
  • 布尔
  • 用户定义的结构

NullReferenceExceptions可能发生的另一种情况是(错误)使用as运算符

class Book {
    public string Name { get; set; }
}
class Car { }

Car mycar = new Car();
Book mybook = mycar as Book;   // Incompatible conversion --> mybook = null

Console.WriteLine(mybook.Name);   // NullReferenceException

在这里, BookCar是不兼容的类型;不能将Car转换 / 铸造为Book 。如果此转换失败,则as返回null 。此后使用mybook会导致NullReferenceException

通常,您应该使用强制转换或as ,如下所示:

如果您期望类型转换始终成功(即您知道对象应该提前),则应使用强制转换:

ComicBook cb = (ComicBook)specificBook;

如果您不确定的类型,但你想尝试使用它作为一个特定的类型,然后使用as

ComicBook cb = specificBook as ComicBook;
if (cb != null) {
   // ...
}

您正在使用包含空值引用的对象。因此,它给出了一个空异常。在示例中,字符串值为 null,并且在检查其长度时,发生了异常。

例:

string value = null;
if (value.Length == 0) // <-- Causes exception
{
    Console.WriteLine(value); // <-- Never reached
}

异常错误是:

未处理的异常:

System.NullReferenceException:对象引用未设置为对象的实例。在 Program.Main()