我有一些代码,当它执行时,它会抛出NullReferenceException,说:
对象引用未设置为对象的实例。
这意味着什么,我可以做什么来修复这个错误?
我有一些代码,当它执行时,它会抛出NullReferenceException,说:
对象引用未设置为对象的实例。
这意味着什么,我可以做什么来修复这个错误?
这意味着您的代码使用了一个设置为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
}
这意味着所讨论的变量没有指向任何对象。我可以这样生成:
SqlConnection connection = null;
connection.Open();
这将引发错误,因为虽然我声明了变量“connection”,但它没有指向任何对象。当我尝试将成员称为“Open”时,没有任何引用可供它解析,它会抛出错误。
要避免此错误,请执行以下操作:
在尝试对对象执行任何操作之前,请始终初始化对象。如果您不确定对象是否为空,请使用object==null进行检查。
JetBrains的ReSharper工具将识别代码中可能出现空引用错误的每个位置,从而允许您进行空检查。这个错误是错误的头号来源,IMHO。
原因是什么?
要旨
您正在尝试使用空值(或VB.NET中的Nothing)。这意味着您要么将其设置为空值,要么从未将其设置任何值。
和其他任何东西一样,null也会被传递。如果在方法“A”中为空,则可能是方法“B”将空传递给了方法“A)。
null可以有不同的含义:
未初始化的对象变量,因此不指向任何对象。在这种情况下,如果访问此类对象的成员,则会导致NullReferenceException。开发人员有意使用null来表示没有可用的有意义的值。注意,C#具有变量的可空数据类型的概念(比如数据库表可以有可空字段)-您可以为它们赋值null,以表示其中没有存储值,例如int?a=空;(这是Nullable<int>a=null;的快捷方式),其中问号表示允许在变量a中存储null。您可以使用if(a.HasValue){…}或if(a==null){..}进行检查。与此示例类似,Nullable变量允许显式地通过.value访问值,也可以通过a正常访问值。请注意,如果a为null,则通过.Value访问它会引发InvalidOperationException而不是NullReferenceException-您应该事先进行检查,即如果您有另一个不可为null的变量int b;那么您应该执行if(a.HasValue){b=a.Value;}或更短的赋值,如果(a!=null){b=a;}。
本文的其余部分将更详细地介绍许多程序员经常犯的错误,这些错误可能会导致NullReferenceException。
更具体地说
引发NullReferenceException的运行时总是意味着相同的事情:您正在尝试使用引用,但该引用未初始化(或者它已初始化,但不再初始化)。
这意味着引用为空,您不能通过空引用访问成员(如方法)。最简单的情况:
string foo = null;
foo.ToUpper();
这将在第二行引发NullReferenceException,因为无法对指向null的字符串引用调用实例方法ToUpper()。
调试
如何查找NullReferenceException的源?除了查看异常本身(它将被准确地抛出在发生异常的位置)之外,Visual Studio中的一般调试规则也适用:放置策略断点并检查变量,方法是将鼠标悬停在变量名称上,打开(快速)观察窗口,或使用各种调试面板(如Locals和Autos)。
如果要查找引用的设置位置,请右键单击其名称并选择“查找所有引用”。然后,您可以在每个找到的位置放置一个断点,并在附加调试器的情况下运行程序。每当调试器在这样的断点上中断时,您都需要确定引用是否为非空,检查变量,并验证它是否指向您期望的实例。
通过以这种方式遵循程序流程,您可以找到实例不应为空的位置,以及为什么未正确设置。
示例
可以引发异常的一些常见情况:
通用的
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.CCurrent可以为null,User属性可以为null或Identity属性可以为空。
间接的
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属性仍然为空。
嵌套集合初始值设定项
public class Person
{
public ICollection<Book> Books { get; set; }
}
public class Book
{
public string Title { get; set; }
}
嵌套集合Initializer的行为相同:
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" });
新Person仅创建Person的实例,但Books集合仍然为空。集合Initializer语法未创建集合对于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.
事件(C#)
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
}
}
(注意:VB.NET编译器会插入事件用法的空检查,因此不必在VB.NET中检查Nothing的事件。)
错误的命名惯例:
如果您对字段的命名与本地变量不同,您可能会意识到您从未初始化过字段。
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的属性时发生异常,则需要了解在返回视图时,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期间按照它们在可视化树中的显示顺序创建的。如果使用事件处理程序等早期创建的控件在InitializeComponent过程中触发,并引用了后期创建的控件,则会引发NullReferenceException。
例如:
<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>
这里,组合框1在label1之前创建。如果comboBox1_SelectionChanged尝试引用“标签1”,则尚未创建它。
private void comboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
label1.Content = comboBox1.SelectedIndex.ToString(); // NullReferenceException here!!
}
更改XAML中声明的顺序(即,在comboBox1之前列出label1,忽略设计理念的问题)至少可以解决此处的NullReferenceException。
使用铸造
var myThing = someObject as Thing;
这不会引发InvalidCastException,但在强制转换失败时(以及someObject本身为null时)返回null。所以要注意这一点。
LINQ FirstOrDefault()和SingleOrDefault(
普通版本First()和Single()在没有异常时抛出异常。在这种情况下,“OrDefault”版本返回null。所以要注意这一点。
前肢
foreach在尝试迭代null集合时抛出。通常由返回集合的方法的意外空结果引起。
List<int> list = null;
foreach(var v in list) { } // NullReferenceException here
更现实的例子是从XML文档中选择节点。如果未找到节点,但初始调试显示所有财产都有效,则将引发:
foreach (var node in myData.MyXml.DocumentNode.SelectNodes("//Data"))
避免的方法
显式检查空值并忽略空值。
如果希望引用有时为空,可以在访问实例成员之前检查引用是否为空:
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;
}
如果值永远不应为空,请使用Debug.Assert,以便在异常发生之前捕获问题。
当您在开发过程中知道一个方法可以返回null,但不应该返回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。
对可为null的值类型使用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中可用):
这有时也被称为安全导航或猫王(以其形状命名)操作员。如果运算符左侧的表达式为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变量为空,如果person.title为空,则不会调用ToUpper。
当然,您仍然需要检查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的引用类型允许显式地允许类型为null。
可以使用csproj文件中的nullable元素为项目设置可为null的注释上下文和可为null警告上下文。此元素配置编译器如何解释类型的可空性以及生成什么警告。有效设置为:
enable:启用可为null的注释上下文。已启用可为null的警告上下文。例如,引用类型(字符串)的变量不可为空。所有可为空警告都已启用。disable:禁用可为null的注释上下文。禁用了可为null的警告上下文。引用类型的变量是不可见的,就像早期版本的C#一样。禁用所有可为null的警告。safeonly:启用了可为null的注释上下文。可为null的警告上下文是安全的。引用类型的变量不可为空。启用所有安全可为零警告。警告:禁用了可为null的注释上下文。已启用可为null的警告上下文。引用类型的变量是不可见的。所有可为空警告都已启用。safetonlywarnings:禁用了可为null的注释上下文。可为null的警告上下文是安全的。引用类型的变量是不可见的。启用所有安全可为零警告。
可为null的引用类型使用与可为null值类型相同的语法进行标注:A?附加到变量的类型。
调试和修复迭代器中的空derefs的特殊技术
C#支持“迭代器块”(在其他一些流行语言中称为“生成器”)。由于延迟执行,在迭代器块中调试NullReferenceException可能特别棘手:
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) { ... }
如果结果为空,则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的调用只返回一个对象,当迭代时,该对象将运行迭代器块。
通过编写这样的空检查,可以防止NullReferenceException,但可以将NullArgumentException移动到迭代点,而不是调用点,这对调试来说非常混乱。
正确的修复方法是:
// 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如何生成NullReferenceException。(这些详细信息适用于在Windows上运行的.NET;其他操作系统使用类似的机制。)
内存在Windows中虚拟化;每个进程都会获得一个由操作系统跟踪的许多“页面”内存组成的虚拟内存空间。内存的每一页上都设置了标志,以确定如何使用:读取、写入、执行等等。最低的一页被标记为“如果以任何方式使用都会产生错误”。
C#中的空指针和空引用在内部都表示为数字零,因此任何试图将其解引用到相应的内存存储中的尝试都会导致操作系统产生错误。然后,.NET运行时检测到此错误并将其转换为NullReferenceException。
这就是为什么取消引用空指针和空引用会产生相同的异常。
第二点呢?取消引用位于虚拟内存最低页的任何无效指针都会导致相同的操作系统错误,从而导致相同的异常。
为什么这有意义?好吧,假设我们有一个包含两个int的结构和一个等于null的非托管指针。如果我们尝试取消引用结构中的第二个int,CLR将不会尝试访问位置0处的存储;它将访问位置4处的存储器。但从逻辑上讲,这是一个空的解引用,因为我们是通过空来到达那个地址的。
如果您使用的是不安全的代码,并且得到了NullReferenceException,请注意,有问题的指针不必为null。它可以是最低页面中的任何位置,将生成此异常。
请注意,无论情况如何,.NET中的原因总是相同的:
您正在尝试使用值为Nothing/null的引用变量。当引用变量的值为Nothing/null时,这意味着它实际上没有保存对堆上存在的任何对象实例的引用。您要么从未向变量赋值,要么从未创建分配给变量的值的实例,要么手动将变量设置为Nothing/null,要么为您调用了将变量设置成Nothing/nnull的函数。
抛出此异常的一个示例是:当您试图检查某个对象时,该对象为空。
例如:
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相比,ArgumentNullException通常在方法期望传递给它的内容不为null时作为防御措施抛出。
更多信息请参见C#NullReferenceException和Null参数。
另一种情况是将空对象转换为值类型。例如,下面的代码:
object o = null;
DateTime d = (DateTime)o;
它将在强制转换时引发NullReferenceException。在上面的示例中,这似乎很明显,但这可能发生在更“后期绑定”的复杂场景中,其中空对象是从您不拥有的某些代码返回的,例如,强制转换是由某些自动系统生成的。
其中一个示例是带有Calendar控件的简单ASP.NET绑定片段:
<asp:Calendar runat="server" SelectedDate="<%#Bind("Something")%>" />
这里,SelectedDate实际上是CalendarWebControl类型的DateTime类型的属性,绑定可以完全返回null。隐式ASP.NET生成器将创建一段与上述转换代码等效的代码。这将引发一个很难发现的NullReferenceException,因为它存在于ASP.NET生成的代码中,这些代码可以很好地编译。。。
您正在使用包含空值引用的对象。所以它给出了一个空异常。在本例中,字符串值为空,在检查其长度时发生异常。
例子:
string value = null;
if (value.Length == 0) // <-- Causes exception
{
Console.WriteLine(value); // <-- Never reached
}
异常错误为:
未处理的异常:System.NullReferenceException:对象引用未设置为实例对象的。位于Program.Main()
另一种可能发生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
在这里,Book和Car是不兼容的类型;汽车不能转换成书。当此强制转换失败时,as返回null。在此之后使用mybook会导致NullReferenceException。
通常,应使用强制转换或,如下所示:
如果您希望类型转换总是成功的(即,您知道对象应该是什么),那么应该使用强制转换:
ComicBook cb = (ComicBook)specificBook;
如果您不确定该类型,但希望尝试将其用作特定类型,请将其用作:
ComicBook cb = specificBook as ComicBook;
if (cb != null) {
// ...
}
更新C#8.02019:可为空的引用类型
C#8.0引入了可为null的引用类型和不可为null引用类型。因此,必须只检查可为null的引用类型,以避免出现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。尽管在处理引用类型时需要保持警惕!
正如名称所暗示的那样,只有引用类型可以保存引用或直接指向任何对象(或“null”)。而值类型始终包含值。
引用类型(必须选中这些类型):
动态对象一串
值类型(您可以忽略这些类型):
数字类型整数类型浮点类型十进制的布尔用户定义的结构
另一个可能收到此异常的一般情况是在单元测试期间模拟类。无论使用何种模拟框架,您都必须确保正确模拟类层次结构的所有适当级别。特别是,被测试代码引用的HttpContext的所有财产都必须进行模拟。
请参阅“测试自定义AuthorizationAttribute时引发的NullReferenceException”,以获取一些详细的示例。
当实体框架中使用的实体的类名与web表单代码隐藏文件的类名相同时,添加一种情况。
假设您有一个web表单Contact.aspx,其代码尾类为Contact,实体名称为Contact。
然后,当您调用context.SaveChanges()时,以下代码将引发NullReferenceException
Contact contact = new Contact { Name = "Abhinav"};
var context = new DataContext();
context.Contacts.Add(contact);
context.SaveChanges(); // NullReferenceException at this line
为了完整起见,DataContext类
public class DataContext : DbContext
{
public DbSet<Contact> Contacts {get; set;}
}
和Contact实体类。有时实体类是分部类,因此您也可以在其他文件中扩展它们。
public partial class Contact
{
public string Name {get; set;}
}
当实体和代码尾类都在同一命名空间中时,会发生错误。要解决此问题,请重命名Contact.aspx的实体类或codebehind类。
原因我仍然不确定原因。但每当任何实体类将扩展System.Web.UI.Page时,都会发生此错误。
有关讨论,请查看DbContext.saveChanges()中的NullReferenceException
虽然导致NullReferenceExceptions的原因和避免/修复此类异常的方法已经在其他答案中得到了解决,但许多程序员尚未学会的是如何在开发过程中独立调试此类异常。
在Visual Studio中,由于Visual Studio调试器,这通常很容易。
首先,确保将捕获正确的错误-请参见如何允许在VS2010中的“System.NullReferenceException”上中断?注释1
然后从调试开始(F5)或将[VS调试器]附加到正在运行的进程。有时,使用Debugger.Break可能很有用,它将提示启动调试器。
现在,当抛出(或未处理)NullReferenceException时,调试器将在发生异常的行上停止(记住上面设置的规则吗?)。有时错误很容易被发现。
例如,在下一行中,唯一可能导致异常的代码是myString求值为null。这可以通过查看观察窗口或在即时窗口中运行表达式来验证。
var x = myString.Trim();
在更高级的情况下,例如以下情况,您需要使用上述技术之一(观察或即时窗口)来检查表达式,以确定str1是否为null或str2是否为null。
var x = str1.Trim() + str2.Trim();
一旦找到了抛出异常的位置,通常很难反向推理,以找出空值被[错误]引入的位置--
花时间了解异常原因。检查空表达式。检查之前可能导致此类空表达式的表达式。根据需要添加断点并单步执行程序。使用调试器。
1如果“抛出时中断”过于激进,并且调试器在.NET或第三方库中的NPE上停止,则可以使用“用户未处理时中断”来限制捕获的异常。此外,VS2012还引入了Just My Code,我建议您也启用它。
如果您在启用“仅我的代码”的情况下进行调试,则行为略有不同。如果启用了“仅我的代码”,调试器将忽略在“我的代码外引发的、不通过“我的码”的第一次公共语言运行时(CLR)异常
当我们试图访问空对象的财产时,或者当字符串值变为空并且我们试图访问字符串方法时,会抛出NullReferenceException。
例如:
访问空字符串的字符串方法时:string str=string.Empty;str.ToLower();//抛出空引用异常访问空对象的属性时:公共类人员{公共字符串名称{get;set;}}人员对象人员;objPerson.Name///抛出空引用异常
我有不同的观点来回答这个问题。这种回答是“我还能做什么来避免它?”
当跨不同层工作时,例如在MVC应用程序中,控制器需要服务来调用业务操作。在这种情况下,依赖注入容器可用于初始化服务以避免NullReferenceException。因此,这意味着您不必担心检查null,只需从控制器调用服务,就好像它们总是可以作为单例或原型使用(并初始化)一样。
public class MyController
{
private ServiceA serviceA;
private ServiceB serviceB;
public MyController(ServiceA serviceA, ServiceB serviceB)
{
this.serviceA = serviceA;
this.serviceB = serviceB;
}
public void MyMethod()
{
// We don't need to check null because the dependency injection container
// injects it, provided you took care of bootstrapping it.
var someObject = serviceA.DoThis();
}
}
Simon Mourier举了一个例子:
object o = null;
DateTime d = (DateTime)o; // NullReferenceException
其中,从对象(或从System.ValueType或System.Enum类之一,或从接口类型)到值类型(非Nullable<>)的拆箱转换(强制转换)本身会产生NullReferenceException。
在另一个方向上,从HasValue等于false的Nullable<>到引用类型的装箱转换可能会给出一个空引用,然后会导致NullReferenceException。典型的例子是:
DateTime? d = null;
var s = d.ToString(); // OK, no exception (no boxing), returns ""
var t = d.GetType(); // Bang! d is boxed, NullReferenceException
有时拳击会以另一种方式进行。例如,对于此非泛型扩展方法:
public static void MyExtension(this object x)
{
x.ToString();
}
以下代码将是有问题的:
DateTime? d = null;
d.MyExtension(); // Leads to boxing, NullReferenceException occurs inside the body of the called method, not here.
出现这些情况是因为运行时在装箱Nullable<>实例时使用的特殊规则。
NullReference异常-Visual Basic
Visual Basic的NullReference异常与C#中的异常没有区别。毕竟,它们都报告了各自使用的.NET Framework中定义的相同异常。Visual Basic特有的原因很少(可能只有一个)。
此答案将使用Visual Basic术语、语法和上下文。所使用的示例来自过去的大量堆栈溢出问题。这是为了通过使用帖子中经常出现的各种情况来最大限度地提高相关性。还为可能需要的人提供了更多的解释。这里很可能列出了一个类似于您的示例。
注:
这是基于概念的:没有代码可以粘贴到项目中。它旨在帮助您了解导致NullReferenceException(NRE)的原因,如何查找它,如何修复它,以及如何避免它。NRE可以通过多种方式导致,因此这不可能是您唯一的遭遇。这些例子(来自StackOverflow文章)并不总是首先展示了做某事的最佳方式。通常,使用最简单的补救方法。
基本含义
消息“Object not set to a instance of Object”表示您正在尝试使用尚未初始化的对象。这归结为以下之一:
您的代码声明了一个对象变量,但它没有初始化它(创建一个实例或“实例化”它)您的代码假设会初始化对象,而不是可能是其他代码过早地使仍在使用的对象无效
查找原因
由于问题是一个Nothing的对象引用,所以答案是检查它们以找出哪一个。然后确定未初始化的原因。将鼠标放在各种变量上,VisualStudio(VS)将显示它们的值——罪魁祸首将是Nothing。
您还应该从相关代码中删除任何Try/Catch块,尤其是Catch块中没有任何内容的代码。这将导致代码在尝试使用Nothing对象时崩溃。这是您想要的,因为它将确定问题的确切位置,并允许您确定导致问题的对象。
Catch中显示Error while…的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定义访问级别,而不是Scope。
有关详细信息,请参阅:
新建操作员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开始,当使用文本和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
有关详细信息,请参阅:
期权推断声明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
有关详细信息,请参见列表(T类)。
数据提供程序对象
使用数据库为NullReference提供了许多机会,因为可以同时使用许多对象(命令、连接、事务、数据集、数据表、数据行……)。注意:无论您使用哪种数据提供程序(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将填充现有数据集,而不是创建数据集。在这种情况下,由于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)
这里有一个错别字:员工与员工。未创建名为“Employee”的DataTable,因此尝试访问该DataTable时会出现NullReferenceException。另一个潜在问题是,假设SQL包含WHERE子句时,存在可能不是这样的Item。
救济
由于这使用一个表,因此使用表(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,而myFoo和Bar也可能是Nothing。补救方法是一次测试一个对象的整个链或路径:
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")
不能引用空对象的任何“下游”。这也适用于控制:
myWebBrowser.Document.GetElementById("formfld1").InnerText = "some value"
此处,myWebBrowser或Document可能为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 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将报告控件在当前上下文中不存在,或者“无法引用非静态成员”。因此,在某种程度上,这是一种只使用VB的情况。它也很复杂,因为它可能导致故障级联。
无法以这种方式初始化数组和集合。此初始化代码将在构造函数创建窗体或控件之前运行。因此:
列表和集合将为空数组将包含五个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。
既然您知道自己在寻找什么,这些应该很容易找到:
“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
...
救济
用Return bList替换函数中的Exit Function。返回空列表与返回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尝试执行清理,但由于无法关闭空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(默认值)时,底部空白/新行中的单元格将全部包含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正在为您管理设置,因此期望它初始化集合是合理的。它会,但前提是您之前已经向集合中添加了初始条目(在设置编辑器中)。由于集合在添加项时(显然)已初始化,因此当“设置”编辑器中没有要添加的项时,集合将保持为“无”。
救济
如果需要,请在表单的Load事件处理程序中初始化设置集合:
If My.Settings.FooBars Is Nothing Then
My.Settings.FooBars = New System.Collections.Specialized.StringCollection
End If
通常,“设置”集合只需要在应用程序首次运行时初始化。另一种补救方法是在Project->Settings|FooBars中向集合添加初始值,保存项目,然后删除假值。
要点
你可能忘记了New操作符。
or
您假设会完美地执行某些操作,将初始化的对象返回到代码中,但没有。
不要忽略编译器警告(永远)并使用Option Strict On(永远)。
MSDN NullReference异常
关于“我该怎么办”的问题,可以有很多答案。
在开发时防止这种错误情况的一种更“正式”的方法是在代码中应用契约设计。这意味着在开发时,需要在系统上设置类不变量,和/或甚至函数/方法前置条件和后置条件。
简而言之,类不变量确保类中有一些约束在正常使用中不会被违反(因此,类不会处于不一致的状态)。前置条件意味着作为函数/方法输入的数据必须遵循某些约束集,并且永远不会违反这些约束,后置条件意味着函数/方法输出必须再次遵循这些约束集,而永远不会违反它们。在执行无错误程序期间,不应违反合同条件,因此,在调试模式下实际检查合同设计,而在发布版本中禁用,以最大化开发的系统性能。
通过这种方式,可以避免由于违反约束集而导致的NullReferenceException情况。例如,如果在类中使用对象属性X,然后尝试调用其方法之一,并且X具有空值,则这将导致NullReferenceException:
public X { get; set; }
public void InvokeX()
{
X.DoSomething(); // if X value is null, you will get a NullReferenceException
}
但是,如果您将“属性X永远不能有空值”设置为方法前提条件,则可以防止前面描述的情况:
//Using code contracts:
[ContractInvariantMethod]
protected void ObjectInvariant()
{
Contract.Invariant(X != null);
//...
}
因此,.NET应用程序存在代码合同项目。
或者,可以使用断言应用契约设计。
更新:值得一提的是,这个词是Bertrand Meyer在设计Eiffel编程语言时创造的。
TL;DR:尝试使用Html.Parate而不是Renderpage
当我试图通过发送模型在视图中呈现视图时,我得到的对象引用未设置为对象的实例,如下所示:
@{
MyEntity M = new MyEntity();
}
@RenderPage("_MyOtherView.cshtml", M); // error in _MyOtherView, the Model was Null
调试显示模型在MyOtherView中为Null。直到我将其更改为:
@{
MyEntity M = new MyEntity();
}
@Html.Partial("_MyOtherView.cshtml", M);
它奏效了。
此外,我没有Html.Paraal的原因是,Visual Studio有时会在Html.Partial下抛出看起来像错误的曲线,如果它位于不同构造的foreach循环中,即使它不是真正的错误:
@inherits System.Web.Mvc.WebViewPage
@{
ViewBag.Title = "Entity Index";
List<MyEntity> MyEntities = new List<MyEntity>();
MyEntities.Add(new MyEntity());
MyEntities.Add(new MyEntity());
MyEntities.Add(new MyEntity());
}
<div>
@{
foreach(var M in MyEntities)
{
// Squiggly lines below. Hovering says: cannot convert method group 'partial' to non-delegate type Object, did you intend to envoke the Method?
@Html.Partial("MyOtherView.cshtml");
}
}
</div>
但我能够运行应用程序,没有出现任何问题。通过将foreach循环的结构更改为如下所示,我能够消除该错误:
@foreach(var M in MyEntities){
...
}
虽然我觉得这是因为Visual Studio误读了符号和括号。
简单来说:
您正在尝试访问未创建或当前不在内存中的对象。
那么如何解决这个问题:
调试并让调试器中断。。。它将直接将您带到被破坏的变量。。。现在你的任务是简单地解决这个问题。。在适当的位置使用新关键字。如果这是由于对象不存在而在某些数据库命令上导致的,那么您需要做的就是执行空检查并处理它:如果(i==空){//处理这个}最难的。。如果GC已经收集了对象。。。如果您试图使用字符串查找对象,通常会发生这种情况。。。也就是说,通过对象的名称查找它,那么GC可能已经清理了它。。。这很难找到,会成为一个很大的问题。。。解决这一问题的更好方法是在开发过程中,在必要时进行空检查。这将为您节省大量时间。
通过名称查找,我的意思是某些框架允许您使用字符串查找对象,代码可能如下:FindObject(“ObjectName”);
你能怎么办?
这里有很多很好的答案来解释空引用是什么以及如何调试它。但是关于如何防止这个问题或者至少让它更容易被发现的问题却很少。
检查参数
例如,方法可以检查不同的参数以查看它们是否为空,并抛出ArgumentNullException,这显然是为此目的创建的异常。
ArgumentNullException的构造函数甚至将参数的名称和消息作为参数,以便您可以确切地告诉开发人员问题所在。
public void DoSomething(MyObject obj) {
if(obj == null)
{
throw new ArgumentNullException("obj", "Need a reference to obj.");
}
}
使用工具
还有几个库可以提供帮助。例如,“Resharper”可以在编写代码时向您提供警告,尤其是当您使用它们的属性:NotNullAttribute时
在“Microsoft代码契约”中,您可以使用Contract.Requals(obj!=null)这样的语法,这为您提供了运行时和编译检查:引入代码契约。
还有“PostSharp”,它允许您只使用如下属性:
public void DoSometing([NotNull] obj)
通过这样做并使PostSharp成为构建过程的一部分,将在运行时检查obj是否为空。参见:PostSharp空检查
普通代码解决方案
或者,您可以始终使用简单的旧代码编写自己的方法。例如,这里有一个可以用来捕获空引用的结构。它是按照与Nullable<T>相同的概念建模的:
[System.Diagnostics.DebuggerNonUserCode]
public struct NotNull<T> where T: class
{
private T _value;
public T Value
{
get
{
if (_value == null)
{
throw new Exception("null value not allowed");
}
return _value;
}
set
{
if (value == null)
{
throw new Exception("null value not allowed.");
}
_value = value;
}
}
public static implicit operator T(NotNull<T> notNullValue)
{
return notNullValue.Value;
}
public static implicit operator NotNull<T>(T value)
{
return new NotNull<T> { Value = value };
}
}
您使用的方式与使用Nullable<T>的方式非常相似,但目的恰恰相反——不允许null。以下是一些示例:
NotNull<Person> person = null; // throws exception
NotNull<Person> person = new Person(); // OK
NotNull<Person> person = GetPerson(); // throws exception if GetPerson() returns null
NotNull<T>隐式转换为T和T,因此您可以在任何需要的地方使用它。例如,您可以将Person对象传递给采用NotNull<Person>的方法:
Person person = new Person { Name = "John" };
WriteName(person);
public static void WriteName(NotNull<Person> person)
{
Console.WriteLine(person.Value.Name);
}
正如您在上面看到的,对于空值,您可以通过value属性访问基础值。或者,您可以使用显式或隐式转换,您可以看到以下返回值的示例:
Person person = GetPerson();
public static NotNull<Person> GetPerson()
{
return new Person { Name = "John" };
}
或者,您甚至可以在方法通过执行强制转换仅返回T(在本例中为Person)时使用它。例如,以下代码与上面的代码类似:
Person person = (NotNull<Person>)GetPerson();
public static Person GetPerson()
{
return new Person { Name = "John" };
}
结合扩展
将NotNull<T>与扩展方法相结合,您可以涵盖更多情况。下面是扩展方法的示例:
[System.Diagnostics.DebuggerNonUserCode]
public static class NotNullExtension
{
public static T NotNull<T>(this T @this) where T: class
{
if (@this == null)
{
throw new Exception("null value not allowed");
}
return @this;
}
}
下面是一个如何使用它的示例:
var person = GetPerson().NotNull();
github
为了便于参考,我在GitHub上提供了上述代码,您可以在以下位置找到:
https://github.com/luisperezphd/NotNull
相关语言功能
C#6.0引入了“空条件运算符”,这有点帮助。使用此功能,您可以引用嵌套对象,如果其中任何一个为空,则整个表达式返回空。
这减少了在某些情况下必须执行的空检查的数量。语法是在每个点前加一个问号。以以下代码为例:
var address = country?.State?.County?.City;
假设country是一个country类型的对象,该对象具有名为State等属性。如果country、State、County或City为空,则地址将变为空。因此,您只需检查地址是否正确。
这是一个很好的功能,但它提供的信息较少。这并不能明显看出4中的哪一个是空的。
像Nullable一样内置?
C#对Nullable<T>有一个很好的简写,你可以在类型后面加一个问号,比如so int?。
如果C#有类似于上面NotNull<T>结构的东西,并且有类似的速记,也许是感叹号(!),这样你就可以写类似于:public void WriteName(Person!Person)的东西了。
当您尝试使用的类的对象未实例化时,会发生NullReferenceException或未设置对象实例的Object引用。例如:
假设您有一个名为Student的班级。
public class Student
{
private string FirstName;
private string LastName;
public string GetFullName()
{
return FirstName + LastName;
}
}
现在,考虑另一个你试图检索学生全名的班级。
public class StudentInfo
{
public string GetStudentName()
{
Student s;
string fullname = s.GetFullName();
return fullname;
}
}
如以上代码所示Student s-只声明Student类型的变量,注意Student类此时未实例化。因此,当执行s.GetFullName()语句时,它将抛出NullReferenceException。
错误行“Object reference not set to an instance of a Object.”表示您尚未将实例对象分配给对象引用,但仍在访问该对象的财产/方法。
例如:假设您有一个名为myClass的类,它包含一个属性prop1。
public Class myClass
{
public int prop1 {get;set;}
}
现在,您正在访问其他类中的prop1,如下所示:
public class Demo
{
public void testMethod()
{
myClass ref = null;
ref.prop1 = 1; // This line throws an error
}
}
上述行引发错误,因为类myClass的引用已声明,但未实例化,或者对象的实例未分配给该类的引用。
要解决这个问题,必须实例化(将对象分配给该类的引用)。
public class Demo
{
public void testMethod()
{
myClass ref = null;
ref = new myClass();
ref.prop1 = 1;
}
}
有趣的是,本页的答案中没有一个提到两种边缘情况:
边缘案例#1:对字典的并发访问
.NET中的泛型字典不是线程安全的,当您尝试从两个并发线程访问密钥时,它们有时可能会抛出NullReference,甚至(更频繁)抛出KeyNotFoundException。在这种情况下,这个例外情况很容易引起误解。
边缘案例#2:不安全代码
如果不安全代码引发NullReferenceException,您可以查看指针变量,并检查它们是否存在IntPtr.Zero或其他内容。这是同一回事(“空指针异常”),但在不安全的代码中,变量通常被转换为值类型/数组等,你会把头撞在墙上,想知道值类型如何抛出此异常。
(顺便说一句,除非您需要,否则不使用不安全代码的另一个原因。)
边缘案例3:Visual Studio多监视器设置,辅助监视器的DPI设置与主监视器不同
此边缘案例是特定于软件的,属于Visual Studio 2019 IDE(以及可能更早的版本)。
一种重现问题的方法:将任何组件从工具箱拖到非主监视器上的Windows窗体上,该窗体的DPI设置与主监视器不同,然后会弹出一个“Object reference not set to A instance of A Object”(对象引用未设置为对象的实例)。根据这个线程,这个问题已经知道了很长一段时间,在编写时仍然没有解决。
这基本上是一个Null引用异常。如Microsoft所述-
尝试访问值为空的类型的成员。
这是什么意思?
这意味着,如果任何成员不具有任何价值,而我们让该成员执行某项任务,那么系统无疑会抛出一条消息,并表示-
“嘿,等等,该成员没有值,因此无法执行您正在移交的任务。”
异常本身表示正在引用某个对象,但未设置其值。因此,这表示它只在使用引用类型时发生,因为Value类型不可为null。
如果使用Value类型成员,则不会发生NullReferenceException。
class Program
{
static void Main(string[] args)
{
string str = null;
Console.WriteLine(str.Length);
Console.ReadLine();
}
}
上面的代码显示了分配了空值的简单字符串。
现在,当我尝试打印字符串str的长度时,我确实收到了“System.NullReferenceException”类型的未处理异常消息,因为成员str指向null,并且不能有任何长度的null。
当我们忘记实例化引用类型时,也会出现“NullReferenceException”。
假设我有一个类和成员方法。我没有实例化我的类,只是命名了我的类。现在,如果我尝试使用该方法,编译器将抛出错误或发出警告(取决于编译器)。
class Program
{
static void Main(string[] args)
{
MyClass1 obj;
obj.foo(); // Use of unassigned local variable 'obj'
}
}
public class MyClass1
{
internal void foo()
{
Console.WriteLine("Hello from foo");
}
}
上述代码的编译器引发一个错误,即变量obj未赋值,这意味着我们的变量有空值或没有值。上述代码的编译器引发一个错误,即变量obj未赋值,这意味着我们的变量有空值或没有值。
为什么会发生这种情况?
NullReferenceException是由于我们没有检查对象的值而导致的。在代码开发中,我们经常不检查对象值。当我们忘记实例化对象时,也会出现这种情况。使用可以返回或设置空值的方法、财产、集合等也可能是此异常的原因。
如何避免?
有多种方式和方法可以避免这一著名的例外:
显式检查:我们应该坚持检查对象、财产、方法、数组和集合是否为null的传统。这可以使用if-else-if-else等条件语句简单地实现。异常处理:管理此异常的重要方法之一。使用简单的try-catch finally块,我们可以控制这个异常,并维护它的日志。这在应用程序处于生产阶段时非常有用。Null操作符:在为对象、变量、财产和字段设置值时,也可以方便地使用Null合并操作符和Null条件操作符。调试器:对于开发人员来说,我们有调试的利器。如果我们在开发过程中遇到NullReferenceException,我们可以使用调试器找到异常的源。内置方法:GetValueOrDefault()、IsNullOrWhiteSpace()和IsNullorEmpty()等系统方法检查空值,如果存在空值,则分配默认值。
这里已经有很多好的答案。你也可以在我的博客上查看更详细的描述和示例。
希望这也有帮助!
实际上,修复NullReferenceExeption的最简单方法有两种。
例如,如果你有一个附带脚本的GameObject和一个名为rb(rigidbody)的变量,那么当你开始游戏时,这个变量将以null开头。这就是为什么会得到NullReferenceExeption,因为计算机中没有存储在该变量中的数据。
我将使用刚体变量作为示例。实际上,我们可以通过以下几种方式轻松地添加数据:
使用AddComponent>Physics>RigidBody将刚体添加到对象然后进入脚本并键入rb=GetComponent<Rigidbody>();这行代码在Start()或Awake()函数下最有效。您可以通过编程方式添加一个组件,同时用一行代码分配变量:rb=AddComponent<RigidBody>();
进一步注意:如果您希望Unity向您的对象添加组件,而您可能忘记添加组件,那么可以在类声明上方键入[RequireComponent(typeof(RigidBody))](所有using下方的空格)。
享受并享受制作游戏的乐趣!
您可以在C#6中使用Null条件运算符以干净的方式修复NullReferenceException,并编写更少的代码来处理空检查。
它用于在执行成员访问(?.)或索引(?[)操作之前测试null。
实例
var name = p?.Spouse?.FirstName;
相当于:
if (p != null)
{
if (p.Spouse != null)
{
name = p.Spouse.FirstName;
}
}
结果是,当p为null或p为null时,该名称将为null。
否则,将为变量名分配p.Spouse.FirstName的值。
有关详细信息:Null条件运算符