原因是什么?
底线
您正在尝试使用null
(或VB.NET中的Nothing
)。这意味着您要么将其设置为null
,要么根本不将其设置为任何东西。
像其他任何内容一样,null
也可以通过。如果在方法“ A”中为null
,则可能是方法“ B”将null
传递给方法“ A”。
null
可以具有不同的含义:
未初始化的对象变量,因此没有指向任何对象。在这种情况下,如果访问此类对象的属性或方法,则会导致NullReferenceException
。
开发人员有意使用null
指示没有可用的有意义的值。注意,C#有可空数据类型的变量(如数据库表可以具有为空的字段)的概念 - 你可以分配null
到它们,以指示没有存储在它的值,例如int? a = null;
其中问号表明它允许将空值存储在变量a
中。您可以使用if (a.HasValue) {...}
或if (a==null) {...}
进行检查。像本例中的a
这样的可空变量允许通过a.Value
显式地访问值,或者像通过a
一样正常地访问值。注意,经由a.Value
访问它引发InvalidOperationException
代替NullReferenceException
如果a
就是null
- 你应该做检查预先,即,如果你有另一对为空的可变int b;
那么应该进行if (a.HasValue) { b = a.Value; }
或更短的if (a != null) { b = a; }
之类的作业。
本文的其余部分将更详细地介绍并显示许多程序员经常犯的错误,这些错误可能导致出现NullReferenceException
。
进一步来说
运行时抛出NullReferenceException
总是意味着同一件事:您正在尝试使用引用,并且该引用未初始化(或者曾经被初始化,但是不再被初始化)。
这意味着引用是null
,并且您不能通过null
引用访问成员(例如方法)。最简单的情况:
string foo = null;
foo.ToUpper();
这将在第二行抛出
NullReferenceException
,因为您不能在指向
ToUpper()
的
string
引用上调用实例方法
null
。
调试
您如何找到
NullReferenceException
的来源?除了看例外本身,这将正好在它出现的位置被抛出,在Visual Studio调试的一般规则:地方战略断点和
inspect your variables,无论是通过将鼠标悬停他们的名字,打开(快速)监视窗口或使用不同的调试面板像当地人和汽车。
如果要查找引用的设置位置或未设置的位置,请右键单击其名称,然后选择“查找所有引用”。然后,可以在每个找到的位置放置一个断点,并在连接了调试器的情况下运行程序。每一次这样的断点调试器符,你需要确定你是否期待参照非空,检查变量,并验证它指向,当你期待它的实例。
通过以这种方式遵循程序流程,您可以找到实例不应为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
}
}
错误的命名约定:
如果您对字段命名的方式不同于本地名称,则可能已经意识到您从未初始化过该字段。
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
在动作方法被设定,当你
return
视图。当从控制器返回空模型(或模型属性)时,视图访问它时会发生异常:
// Controller
public class Restaurant:Controller
{
public ActionResult Search()
{
return View(); // Forgot the provide a Model here.
}
}
// Razor view
@foreach (var restaurantSearch in Model.RestaurantSearch) // Throws.
{
}
<p>@Model.somePropertyName</p> <!-- Also throws -->
WPF控件创建顺序和事件
WPF控件是在调用
InitializeComponent
的过程中按照它们在视觉树中出现的顺序创建的。对于带有事件处理程序等的早期创建的控件,将在
NullReferenceException
期间触发,引用早期创建的控件,则引发
InitializeComponent
。
例如 :
<Grid>
<!-- Combobox declared first -->
<ComboBox Name="comboBox1"
Margin="10"
SelectedIndex="0"
SelectionChanged="comboBox1_SelectionChanged">
<ComboBoxItem Content="Item 1" />
<ComboBoxItem Content="Item 2" />
<ComboBoxItem Content="Item 3" />
</ComboBox>
<!-- Label declared later -->
<Label Name="label1"
Content="Label"
Margin="10" />
</Grid>
在此,在
comboBox1
之前创建
label1
。如果
comboBox1_SelectionChanged
尝试引用`label1,则尚未创建。
private void comboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
label1.Content = comboBox1.SelectedIndex.ToString(); // NullReference here!!
}
更改XAML中声明的顺序(即在
label1
之前列出
comboBox1
而不考虑设计哲学问题)至少会在这里解决
NullReferenceException
。
使用
as
进行投射
var myThing = someObject as Thing;
当强制转换失败时(并且someObject本身为null),这不会引发InvalidCastException,而是返回
null
。因此请注意。
LINQ FirstOrDefault()和SingleOrDefault()
普通版本
First()
和
Single()
抛出异常时没有什么。在这种情况下,“ OrDefault”版本返回null。因此请注意。
前言
当您尝试迭代null集合时,
foreach
引发。通常由返回集合的方法的意外
null
结果引起。
List<int> list = null;
foreach(var v in list) { } // exception
更实际的示例-从XML文档中选择节点。如果未找到节点,但初始调试显示所有属性均有效,则会抛出该异常:
foreach (var node in myData.MyXml.DocumentNode.SelectNodes("//Data"))
避免方法
显式检查
null
并忽略空值。
如果您期望引用有时为空,则可以在访问实例成员之前检查它是否为
null
:
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.
}
尽管此检查
will not end up in your release build,导致在发布模式下运行时
NullReferenceException
再次引发
book == 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中可用):
有时也称为安全导航或Elvis(形状正确)操作员。如果运算符左侧的表达式为null,则不会计算右侧,而是返回null。这意味着像这样的情况:
var title = person.Title.ToUpper();
如果此人没有标题,这将引发异常,因为它试图在具有空值的属性上调用
ToUpper
。
在C#5及以下版本中,可以通过以下方式进行保护:
var title = person.Title == null ? null : person.Title.ToUpper();
现在,title变量将为null而不是引发异常。 C#6为此引入了一个较短的语法:
var title = person.Title?.ToUpper();
这将导致标题变量为
null
,如果
ToUpper
为
person.Title
,则不会调用
null
。
当然,您仍然必须检查
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];
并返回第ith个元素。
使用空上下文(在C#8中可用):
在C#8中引入了null上下文和nullable引用类型,它们对变量执行静态分析,并在值可能为null或已设置为null时提供编译器警告。可为空的引用类型允许将类型明确地允许为null。
可以使用csproj文件中的Nullable元素为项目设置可为空的注释上下文和可为空的警告上下文。该元素配置编译器如何解释类型的可空性以及生成什么警告。有效设置为:
enable:启用可空注释上下文。可空预警范围内启用。引用类型,串例如变量,是不可为空。启用所有可空性警告。
disable:可空注释上下文已禁用。可空预警范围内被禁止。引用类型的变量是忽略的,就像C#的早期版本一样。所有为空的警告被禁用。
safeonly:启用了可为空的注释上下文。可为空的警告上下文仅是安全的。引用类型的变量不可为空。启用所有安全性为空的警告。
警告:可空注释上下文已禁用。可空预警范围内启用。引用类型的变量是忽略的。启用所有可空性警告。
safeonlywarnings:可空注释上下文被禁用。可为空的警告上下文仅是安全的。
引用类型的变量是忽略的。启用所有安全性为空的警告。
使用与可为空的值类型相同的语法来记录可为空的引用类型:将
?
附加到变量的类型。
用于调试和修复迭代器中的空deref的特殊技术
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
的调用仅返回一个对象,该对象在进行迭代时将运行迭代器块。
通过编写空校验这样可以防止空取消引用,但您将空参数异常迭代的点,而不是呼叫的一点,那是非常混乱调试。
正确的解决方法是:
// 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的参考源,您会发现该技术始终被使用。编写起来有点笨拙,但是它使调试null错误更加容易。优化代码以方便调用者,而不是作者。
关于不安全代码中的空取消引用的说明
顾名思义,C#具有“不安全”模式,这是非常危险的,因为没有强制执行提供内存安全和类型安全的常规安全机制。除非您对内存的工作原理有透彻和深入的了解,否则不要编写不安全的代码。
在不安全模式下,您应该意识到两个重要事实:
取消引用空指针会产生与取消引用空引用相同的异常
取消引用无效的非null指针会产生该异常
在某些情况下,
要了解为什么会这样,它有助于首先了解.NET如何产生null取消引用异常。 (这些详细信息适用于Windows上运行的.NET;其他操作系统使用类似的机制。)
存储虚拟化是在Windows中;每个进程都会获得由操作系统跟踪的许多“页面”内存的虚拟内存空间。内存的每个页面上都设置了标志,这些标志确定如何使用它:读取,写入,执行等。最低的页面标记为“如果以任何方式使用都会产生错误”。
C#中的空指针和空引用在内部都表示为数字零,因此任何尝试将其取消引用到其相应的内存中的操作都会导致操作系统产生错误。然后,.NET运行时将检测到此错误,并将其转变为null取消引用异常。
这就是为什么同时取消引用空指针和空引用会产生相同的异常的原因。
那第二点呢?取消引用落在虚拟内存最低页中的任何无效指针会导致相同的操作系统错误,从而导致相同的异常。
为什么这有意义?好吧,假设我们有一个包含两个int的结构,以及一个等于null的非托管指针。如果我们尝试取消引用结构中的第二个int,CLR将不会尝试访问零位置的存储;它将访问第四位置的存储。但从逻辑上讲,这是一个空取消引用,因为我们要通过空到达该地址。
如果您使用的是不安全的代码,并且会收到null解除引用异常,则请注意,有问题的指针不必为null。它可以在最低的页中的任何位置,这个异常会产生。
我是一名优秀的程序员,十分优秀!