什么是NullReferenceException,并且我该如何修复它?

29 浏览
0 Comments

什么是NullReferenceException,并且我该如何修复它?

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

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

这是什么意思,我该怎么修复这个错误?

admin 更改状态以发布 2023年5月21日
0
0 Comments

NullReference Exception — Visual Basic

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

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

注意:

  1. 这是基于概念的:没有要粘贴到项目中的代码。旨在帮助您理解何时引起NullReferenceException(NRE)、如何找到它、如何修复它以及如何避免它。 NRE可以有很多种原因,因此这不太可能是您的唯一遭遇。
  2. 示例(来自Stack Overflow帖子)并不总是显示首选的操作方法。
  3. 通常使用最简单的补救措施。

基本含义

消息“Object not set to an instance of Object”意味着您正在尝试使用未被初始化的对象。这可以归结为以下之一:

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

找到原因

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

IDE debug display

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

在Catch中显示Error while...MsgBox帮助不大。此方法还会导致非常糟糕的Stack Overflow问题,因为您无法描述实际的异常、涉及的对象甚至发生它的代码行。

您还可以使用Locals Window调试->Windows->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,而你将在其他地方使用的具有模块级 Scopereg变量仍将为 Nothing

忘记使用 New 运算符是 Stack  Overflow 问题中出现 NullReference 异常的最主要原因

Visual Basic 反复使用 New 来使这个过程清晰明了:使用 New 运算符创建一个新的对象并调用 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 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 Option Strict:

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

例子:Class对象的数组

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

有关更多信息,请参见:


Lists and Collections

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


Data Provider Objects

与数据库一起工作会出现许多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)

这里是一个打字错误: EmployeesEmployee。 没有名为 "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
   ...

代码仅测试 Items,而 myFooBar 也可能为空。 解决方法是一次测试一个对象的整个链或路径:

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 可能为空,或者 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

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

无法使用这种方法初始化数组和集合。这个初始化代码将在构造函数创建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

数组代码可能还没有解脱。 每个在容器控件(如GroupBoxPanel)中的控件将不会在Me.Controls中找到;它们将在该Panel或GroupBox的 Controls 集合中。 当控件名称拼写错误("TeStBox2")时,控件也不会返回。 在这些情况下,这些数组元素中再次存储Nothing,并且尝试引用它时会导致NRE。

现在您知道正在寻找什么,因此这些应该很容易找到:

VS shows you the error of your ways

"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时使用返回值的内容将导致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实现不当

糟糕的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值表示对象表示缺失或不存在的数据。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

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

解决方法

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"

如果dgvBooksAutoGenerateColumns=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

当您的DataGridViewAllowUserToAddRows=True(默认情况下)时,底部的空/新行中的Cells将全部包含Nothing。大多数尝试使用其内容(例如ToString)将导致NRE。

解决方法

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

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正在为您管理设置,因此可以合理地期望它初始化集合。它会这样做,但仅当您先前添加了初始条目到集合(在Settings编辑器中)时。由于集合(显然)在添加项目时初始化,因此当Settings编辑器中没有项目可添加时,它仍然为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 Exception

0