提问者:小点点

什么是NullReferenceException,如何修复它?


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

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

这意味着什么?我可以做什么来修复这个错误?


共3个答案

匿名用户

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

与其他任何东西一样,null也会被传递。 如果方法“a”中的null,则可能是方法“B”向方法“a”传递了一个null

null可以有不同的含义:

    1. 对象变量,这些变量未初始化,因此没有指向任何内容。 在这种情况下,如果访问此类对象的属性或方法,将导致NullReferenceException

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

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

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

string foo = null;
foo.ToUpper();

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

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

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

通过以这种方式遵循程序流,您可以找到实例不应该为空的位置,以及为什么它没有正确设置。

可以引发异常的一些常见场景:

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可以为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
    }
}

###Bad Naming Conventions:

If you named fields differently from locals, you might have realized that you never initialized the field. 

public class Form1{private

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;
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!";
    }
}
// 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视图中引用@model的属性时发生异常,则需要了解,当返回视图时,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控件是在调用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

var myThing = someObject as Thing;

这不会引发InvalidCastException,而是在强制转换失败时(以及SomeObject本身为null时)返回null。 所以要注意这一点。

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

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

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

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

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

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

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

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

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

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

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()在它确实发生时尽快中断:

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

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时提供默认值的速记方法:

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,那么右侧将不进行计算,而是返回null。 这意味着像这样的情况:

var title = person.Title.ToUpper();

如果person没有标题,这将引发异常,因为它试图对具有空值的属性调用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中引入了null上下文和可为null的引用类型,它们对变量执行静态分析,并在某个值可能为null或已设置为null时提供编译器警告。 可为空的引用类型允许显式地允许类型为空。

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

  • 启用:启用可为空的批注上下文。 启用可为Null的警告上下文。 引用类型的变量(例如字符串)不可为空。 启用所有可为空的警告。
  • 禁用:禁用可为空的批注上下文。 禁用可为null的警告上下文。 引用类型的变量是被忽略的,就像C#的早期版本一样。 禁用所有可为空的警告。
  • SafeOnly:启用了可为null的批注上下文。 可为null的警告上下文是仅安全的。 引用类型的变量不可为空。 启用所有安全可为空警告。
  • 警告:可为null的批注上下文被禁用。 启用可为Null的警告上下文。 引用类型的变量被忽略。 启用所有可为空的警告。
  • SafeonlyWarnings:禁用了可为空的批注上下文。 可为null的警告上下文是仅安全的。 引用类型的变量被忽略。 启用所有安全可为空警告。

可为空的引用类型使用与可为空的值类型相同的语法进行注释:将附加到变量的类型之后。

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检查,您可以防止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();
}

也就是说,创建一个具有迭代器块逻辑的私有helper方法,以及一个执行空检查并返回迭代器的公共surface方法。 现在,当调用getFrobs时,空检查立即发生,然后在迭代序列时执行getFrobsForReal

如果您检查对象的LINQ的引用源,您将看到整个过程中都使用了这种技术。 编写起来稍微笨拙一些,但它使调试nultility错误变得容易得多。 为了调用者的方便而优化你的代码,而不是为了作者的方便。

C#具有“不安全”模式,顾名思义,这是极其危险的,因为提供内存安全和类型安全的正常安全机制没有得到实施。 除非您对内存的工作原理有一个全面而深刻的理解,否则您不应该编写不安全的代码。

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

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

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

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

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

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

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

这为什么说得通呢? 假设我们有一个包含两个int的结构,和一个等于NULL的非托管指针。 如果我们试图取消引用结构中的第二个int,clr将不会尝试访问位置零的存储; 它会进入四号地点的储存库。 但从逻辑上讲,这是一个null取消引用,因为我们是通过null到达那个地址的。

如果您正在处理不安全的代码,并且您得到了一个null取消引用异常,只需注意有问题的指针不必是null。 它可以是最低页中的任何位置,将产生此异常。

匿名用户

Visual Basic的NullReference异常与C#中的异常没有什么不同。 毕竟,它们都在报告在它们都使用的。NET Framework中定义的相同异常。 Visual Basic特有的原因很少(可能只有一种)。

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

注意:

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

消息“object not set to an instance of object”表示您正在尝试使用尚未初始化的对象。 这可以归结为其中一个:

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

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

您还应该从相关代码中删除任何try/Catch块,特别是Catch块中没有任何内容的代码。 这将导致代码在尝试使用Nothing的对象时崩溃。 这是您想要的,因为它将确定问题的确切位置,并允许您确定引起问题的对象。

Catch中的msgbox显示错误,而...将帮助不大。 这种方法还会导致非常糟糕的堆栈 ; 溢出问题,因为您不能描述实际的异常,所涉及的对象,甚至是发生异常的代码行。

您还可以使用Locals窗口(debug->windows->Locals)检查对象。

一旦你知道问题是什么和在哪里,它通常是相当容易修复,而且比张贴一个新问题更快。

另请参阅:

  • 断点
  • MSDN:如何:使用try/Catch块捕获异常
  • MSDN:例外情况的最佳实践
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)中。 具有模块级别作用域reg变量(您将在其他任何地方使用)仍然是nothing

缺少new运算符是堆栈中出现的NullReference异常的首要原因 ; 审查了溢出问题。

Visual Basic试图反复使用new使过程清晰:使用new运算符创建新对象并调用Sub new(构造函数),对象可以在其中执行任何其他初始化。

明确地说,dim(或private)只声明一个变量及其类型。 变量的作用域--它是存在于整个模块/类中,还是在过程中是本地的--由它的声明位置决定。 Private Friend Public定义访问级别,而不是作用域。

有关更多信息,请参见:

  • 新运算符
  • Visual Basic中的作用域
  • Visual Basic中的访问级别
  • 值类型和引用类型

数组也必须实例化:

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开始,当使用literal和选项infer初始化本地数组时,asnew元素是可选的:

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}

数据类型和数组大小是从被分配的数据中推断出来的。 类/模块级声明仍然需要aswithoption 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

有关更多信息,请参见:

  • 选项推断语句
  • Visual Basic中的作用域
  • Visual Basic中的数组

。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)

常见的监督是使用集合类型的类:

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提供了许多机会,因为可能存在许多对象(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数据集对象,但从未创建实例。 DataAdapter将填充现有的数据集,而不是创建一个。 在本例中,由于ds是局部变量,IDE警告您可能发生这种情况:

当声明为模块/类级变量时,就像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)

这里有一个错误:employeesvsemployees。 没有创建名为“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是一个函数,返回受影响的行数,也可以进行测试:

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,而myfoobar也可能什么都不是。 补救办法是一次测试一个对象的整个链或路径:

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

和其他是很重要的。 一旦遇到第一个false条件,将不会执行后续测试。 这允许代码一次安全地“钻入”对象一个“级别”,只在确定myfoo有效之后(以及如果)才计算myfoo.bar。 在对复杂对象进行编码时,对象链或路径可能会变得相当长:

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

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

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

在这里,MyWebBrowserDocument可以为Nothing,或者FormFLD1元素可能不存在。

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不是Nothing),还可以使用...

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会报告控件在当前上下文中不存在,或者“无法引用非静态成员”。 所以,在某种程度上,这是一个只使用VB的情况。 它也很复杂,因为它可能导致故障级联。

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

  • 列表和集合将简单地为空
  • 数组将包含五个nothing元素
  • somevar赋值将导致立即的NRE,因为没有任何东西没有.text属性

稍后引用数组元素将导致NRE。 如果您在form_load中这样做,由于一个奇怪的bug,IDE可能不会在发生异常时报告异常。 稍后当您的代码尝试使用该数组时,将弹出异常。 这个“沉默的例外”在本帖中有详细介绍。 就我们的目的而言,关键是当创建表单时发生了灾难性的事情(Sub Newform Load事件),异常可能不会报告,代码退出过程并只显示表单。

由于Sub NewForm 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

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

“Button2”驻留在面板

补救

使用控件引用,而不是使用窗体的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
      ...

补救

将函数中的exit function替换为return blist。 返回空的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尝试执行清理,但由于您无法关闭DataReader对象,因此会产生一个全新的NullReferenceException

一个空的catch块就是魔鬼的游乐场。 这个操作很难理解为什么他会在finally块中获得一个NRE。 在其他情况下,空的catch可能会导致更下游的其他内容混乱,并导致您花费时间在问题所在的错误位置查看错误的内容。 (上面介绍的“无声例外”提供了同样的娱乐价值。)

补救

不要使用空的try/catch块--让代码崩溃,这样您就可以a)确定原因b)确定位置c)应用适当的补救方法。try/catch块并不是为了向唯一有资格修复异常的人--开发人员--隐藏异常。

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

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

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

补救

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

和前面一样,您可以什么都不测试,然后测试一个特定的值:

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复选框(或存在于GroupBox中),则chk将为空,尝试引用任何属性将导致异常。

补救

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

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

DataGridViewAllowUserToAddRows设为True(默认值)时,底部空白/新建行中的单元格将全部包含Nothing。 大多数使用内容的尝试(例如,toString)将导致NRE。

补救

使用for/eace循环并测试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项可能会在第一次使用时导致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->中向集合添加初始值; 设置foobar,保存项目,然后删除假值。

您可能忘记了new运算符。

您以为可以完美无缺地将初始化对象返回到代码中的一些事情并没有这样做。

不要忽略编译器警告(ever),使用option Strict on(always)。

MSDN NullReference异常

匿名用户

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

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

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

这方面的一个示例是使用Calendar控件的简单ASP.NET绑定片段:

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

这里,SelectedDate实际上是CalendalWeb控件类型的DateTime类型的属性,绑定可以完美地返回空值。 隐式ASP.NET生成器将创建一段代码,该代码将等效于上面的强制转换代码。 这将引发一个很难识别的NullReferenceException,因为它存在于ASP.NET生成的代码中,可以很好地编译。。。

相关问题