协慌网

登录 贡献 社区

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

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

你调用的对象是空的。

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

答案

原因是什么?

底线

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

像其他任何东西一样, null被传递。如果是null 的方法 “A”,这可能是因为方法 “B” 通过一个null 的方法 “A”。

null可以有不同的含义:

  1. 对象变量未初始化 ,因此指向无。在这种情况下,如果访问此类对象的属性或方法,则会导致NullReferenceException 。
  2. 开发人员故意使用null表示没有可用的有意义值。请注意,C#具有变量的可空数据类型的概念(如数据库表可以具有可空字段) - 您可以为它们分配null以指示其中没有存储值,例如int? a = null;其中问号表示允许在变量a存储 null。您可以使用if (a.HasValue) {...}或if (a==null) {...} 。可空变量,像a本例中,允许访问通过价值a.Value通过明确,或者只是正常的a 。
    请注意 ,如果a为null ,则通过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 ,因为您无法在指向null的string引用上调用实例方法ToUpper() 。

调试

你如何找到NullReferenceException的来源?除了查看将在其发生位置准确抛出的异常本身之外,Visual Studio 中调试的一般规则适用:放置战略断点并检查变量 ,方法是将鼠标悬停在其名称上,打开(快速)观察窗口或使用各种调试面板,如本地和汽车。

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

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

例子

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

通用

ref1.ref2.ref3.member

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

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

具体来说,在HttpContext.Current.User.Identity.Name , HttpContext.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)null 引用,可以在父(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>

这里comboBox1在label1之前创建。如果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。所以要注意这一点。

的 foreach

当您尝试迭代 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

使用 null 合并运算符: ?? [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;
}

使用 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 ,并且如果person.Title为null则不会调用ToUpper 。

当然,您仍然需要检查 null 的title ,或者使用 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 个元素。

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

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

// 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 参数异常移动到迭代点,而不是调用点,这对调试来说非常混乱 。

正确的解决方法是:

// 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 的参考源,您将看到始终使用此技术。写入稍微笨拙,但它使调试无效性错误变得更加容易。 优化代码以方便调用者,而不是作者的便利 。

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

C#具有 “不安全” 模式,顾名思义,这种模式极其危险,因为不强制执行提供存储器安全性和类型安全性的正常安全机制。 除非您对内存的工作原理有深入的了解,否则不应编写不安全的代码 。

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

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

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

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

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

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

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

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

如果您正在使用不安全的代码并且您获得了空解除引用异常,请注意违规指针不必为空。它可以是最低页面中的任何位置,并且将生成此异常。

NullReference 异常 - Visual Basic

Visual Basic的NullReference Exception与C#中的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 中的一个MsgBox显示Error while...将没什么帮助。此方法还会导致非常糟糕的 Stack Overflow 问题,因为您无法描述实际的异常,涉及的对象甚至代码行。

您还可以使用 “ Locals Window (“ 调试” - >“Windows” - >“局部” )来检查对象。

一旦你知道问题是什么以及在哪里,它通常很容易修复,并且比发布新问题更快。

也可以看看:

例子和补救措施

类对象 / 创建实例

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 ,它仅存在于该上下文(sub)中。具有模块级别Scope的reg变量,您将在其他地方使用它仍然是Nothing 。

缺少New运算符是在审查的 Stack Overflow 问题中看到的NullReference Exceptions的NullReference Exceptions原因 。

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}

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

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只是声明,而不是实例化。创建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 提供了许多机会,因为可以同时使用许多对象( Command , Connection , Transaction , Dataset , DataTable , DataRows ....)。 注意:使用哪个数据提供程序无关紧要 - 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与Employee 。没有创建名为 “Employee” 的DataTable ,因此尝试访问NullReferenceException 。另一个潜在的问题是假设当 SQL 包含 WHERE 子句时会有可能不是这样的Items 。

补救

由于这使用一个表,因此使用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
   ...

代码只测试Items而myFoo和Bar也可能都没有。 解决方法是一次测试一个对象的整个链或路径:

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

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

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

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

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

这里, myWebBrowser或Document可能是 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 Forms

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 的情况。它也很复杂,因为它可能导致级联故障。

无法以这种方式初始化数组和集合。此初始化代码将在构造函数创建Form或Controls 之前运行。结果是:

  • 列表和集合将是空的
  • 该数组将包含 Nothing 的五个元素
  • somevar赋值将导致立即 NRE,因为 Nothing 没有.Text属性

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

由于您的Sub New或Form Load事件中的其他代码不会在 NRE 之后运行,因此许多其他内容可以保持未初始化状态。

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找不到容器控件中的任何控件(如GroupBox或Panel ); 它们将位于该 Panel 或 GroupBox 的 Controls 集合中。当控件名拼写错误时,也不会返回控件( "TeStBox2" )。在这种情况下, Nothing将再次存储在那些数组元素中,并且当您尝试引用它时将产生 NRE。

现在你知道你在寻找什么,这些应该很容易找到: VS向您显示您的方式错误

“Button2” 驻留在Panel

补救

使用Controls引用,而不是使用表单的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时尝试使用返回的任何内容都将导致 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

一个执行不当的 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空DataReader对象,因此会产生全新的NullReferenceException 。

一个空的Catch街区是魔鬼的游乐场。这个 OP 感到困惑,为什么他在Finally块中获得了 NRE。在其他情况下,空的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

如果找不到具有chkName的CheckBox (或存在于GroupBox ),则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将AllowUserToAddRows为True (默认值)时,底部空白 / 新行中的Cells将全部包含Nothing 。大多数使用内容的尝试(例如, ToString )将导致 NRE。

补救

使用For/Each循环并测试IsNewRow属性以确定它是否是最后一行。这适用于AllowUserToAddRows是否为真:

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集合。另一种方法是在Project - > Settings | 中为集合添加初始值FooBars ,保存项目,然后删除假值。


关键点

您可能忘记了New运算符。

要么

你假设的东西可以完美无瑕地将初始化对象返回到你的代码,但事实并非如此。

不要忽略编译器警告(永远)并使用Option Strict On (始终)。


MSDN NullReference 异常

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

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

它会在NullReferenceException抛出NullReferenceException 。在上面的示例中似乎很明显,但这可能发生在更多 “后期绑定” 错综复杂的场景中,其中 null 对象已从您不拥有的某些代码返回,并且转换例如由某个自动系统生成。

其中一个例子是这个带有 Calendar 控件的简单 ASP.NET 绑定片段:

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

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