什么是NullReferenceException,并且我该如何修复它?
什么是NullReferenceException,并且我该如何修复它?
我有一些代码,当它执行时,会抛出一个 NullReferenceException
,说:
对象引用未设置为对象的实例。
这是什么意思,我该怎么修复这个错误?
NullReference Exception — Visual Basic
Visual Basic中的NullReference Exception
与C#中的相同。毕竟,它们都报告了.NET Framework中定义的相同异常,它们都使用它。与Visual Basic有关的原因很少(也许只有一个)。
本答案将使用Visual Basic术语、语法和上下文。所使用的示例来自大量过去的Stack Overflow问题。这是通过使用帖子中经常看到的情况来最大化相关性。还提供了更多的解释,以帮助可能需要的人。与您类似的示例很可能在此列出。
注意:
- 这是基于概念的:没有要粘贴到项目中的代码。旨在帮助您理解何时引起
NullReferenceException
(NRE)、如何找到它、如何修复它以及如何避免它。 NRE可以有很多种原因,因此这不太可能是您的唯一遭遇。 - 示例(来自Stack Overflow帖子)并不总是显示首选的操作方法。
- 通常使用最简单的补救措施。
基本含义
消息“Object not set to an instance of Object”意味着您正在尝试使用未被初始化的对象。这可以归结为以下之一:
- 代码声明了一个对象变量,但没有初始化它(创建实例或“实例化”它)
- 您的代码假定会初始化对象,但实际上没有
- 可能,其他代码过早地使仍在使用中的对象无效。
找到原因
由于问题是对象引用为Nothing
,因此答案是检查它们以找出是哪一个。然后确定为什么没有初始化。将鼠标悬停在各种变量上,Visual Studio(VS)将显示它们的值-罪魁祸首将是Nothing
。
您还应该从相关代码中删除任何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
,而你将在其他地方使用的具有模块级 Scope
的 reg
变量仍将为 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的机会,因为同时可能会使用许多对象(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会警告您可能会发生这种情况:
当被声明为模块/类级变量时,例如 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
。另一个潜在的问题是假设会有 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
,而 myFoo
和 Bar
也可能为空。 解决方法是一次测试一个对象的整个链或路径:
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"
在这里,myWebBrowser
或 Document
可能为空,或者 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的情况。 它也很复杂,因为它可能导致失败级联。
无法使用这种方法初始化数组和集合。这个初始化代码将在构造函数创建Form
或Controls
之前运行。 所以会导致:
- 列表和集合将为空
- 数组将包含五个元素,值均为Nothing
somevar
分配将立即导致NRE,因为Nothing没有.Text
属性
稍后引用数组元素将导致NRE。 如果您在Form_Load
中这样做,由于一个奇怪的错误,当异常发生时IDE可能不会报告它。 异常将在稍后的代码中尝试使用数组时弹出。这个“静默异常”在此帖子中有详细说明。 对于我们的目的,关键是当创建表单(Sub New
或Form Load
事件)时发生灾难性事件时,可能会出现未报告的异常,代码会退出过程并仅显示表单。
由于在NRE之后您的Sub New
或Form 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
数组代码可能还没有解脱。 每个在容器控件(如GroupBox
或Panel
)中的控件将不会在Me.Controls
中找到;它们将在该Panel或GroupBox的 Controls
集合中。 当控件名称拼写错误("TeStBox2"
)时,控件也不会返回。 在这些情况下,这些数组元素中再次存储Nothing
,并且尝试引用它时会导致NRE。
现在您知道正在寻找什么,因此这些应该很容易找到:
"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
如果找不到名为
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
为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
。