原因是什么?
底线
您正在尝试使用
null(或
Nothing在VB.NET中)。这意味着您要么将其设置为
null,要么根本不将其设置为任何东西。
像其他任何东西一样,
null被传递出去。如果
null在 方法“ A”中,则可能是方法“ B”将a传递
null给 方法“ A”。
null可以具有不同的含义:
- 未初始化的 对象变量,因此 没有指向任何对象。 在这种情况下,如果访问此类对象的属性或方法,则会导致
NullReferenceException
。
- 未初始化的 对象变量,因此 没有指向任何对象。 在这种情况下,如果访问此类对象的属性或方法,则会导致
- 开发人员 有意 使用它
null
来指示没有可用的有意义的值。请注意,C#具有变量的可为空的数据类型的概念(例如数据库表可以具有可为空的字段)-您可以分配null
给它们以指示其中没有存储任何值,例如int? a = null;
问号指示允许在其中存储null。可变的a
。您可以使用if (a.HasValue) {...}或使用进行检查if (a==null) {...}。像a
本例一样,可空变量允许通过a.Value
显式访问值,或者像通过一样正常访问值a
。
注意
的是通过访问它a.Value
抛出InvalidOperationException
,而不是NullReferenceException
如果a
ISnull
-您应该事先进行检查,即,如果您有另一个on-
nullable变量,int b;
则应该进行likeif (a.HasValue) { b = a.Value; }或更短的赋值if (a !=null) { b = a; }。
- 开发人员 有意 使用它
本文的其余部分将更详细地说明错误,并指出许多程序员经常犯的错误,这些错误可能导致程序错误
NullReferenceException。
进一步来说
该
runtime扔
NullReferenceException总是 意味着同样的事情:你要使用的引用,引用未初始化(或它 一次
初始化,但 不再 初始化)。
这意味着引用为
null,并且您无法通过
null引用访问成员(例如方法)。最简单的情况:
string foo = null;foo.ToUpper();
这将
NullReferenceException在第二行抛出,因为您无法
ToUpper()在
string指向的引用上调用实例方法
null。
调试
您如何找到来源
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; }}嵌套集合的
Initializers行为相同:
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" });该
newPerson只创建一个实例
Person,但
Books集合仍然是
null。集合
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.大事记
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.公共类Form1 {私人客户客户;
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 NullReferenceExceptionstring firstName = Session["FirstName"].ToString();
ASP.NET MVC空视图模型
如果引用的属性时出现异常
@Model的一个
ASP.NET MVCView,你需要了解的是,
Model获取你的操作方法设定,当您
return的视图。当从控制器返回空模型(或模型属性)时,视图访问它时会发生异常:
// Controllerpublic 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带有事件处理程序等的早期创建的控件,将引发A,而在事件控件等期间触发
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;
当强制转换失败(并且本身为null)时,此方法不会抛出an,
InvalidCastException但会返回a
。因此请注意。
null``someObject
LINQ FirstOrDefault()
和SingleOrDefault()
普通版本
First()并
Single()在没有内容时引发异常。在这种情况下,“ OrDefault”版本返回null。因此请注意。
前言
foreach尝试迭代null集合时引发。通常由
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 pre return book.Title; // Will never throw NullReferenceException in Debug mode.}尽管此检查将不会在您的发行版本中结束,但会导致它在发行版模式下运行时
NullReferenceException再次引发
book == null。
使用GetValueOrDefault()
的nullable
值类型提供一个默认值,当他们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遇到a时提供默认值的简写:
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();
这将导致title变量为
null,
ToUpper如果
person.Title为,则不会调用
null。
当然,您 仍然 必须检查
titlenull或将null条件运算符与null合并运算符(
??)一起使用以提供默认值:
// regular null checkint titleLength = 0;if (title != null) titleLength = title.Length; // If title is null, this would throw NullReferenceException// combining the `?` and the `??` operatorint 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<sup>th</sup>元素。
使用空上下文(在C#8中可用):
其中引入了
C#8null上下文和nullable引用类型,它们对变量执行静态分析,并在值可能为null或已设置为null时向编译器发出警告。可为空的引用类型允许将类型明确地允许为null。
可以使用文件中的
Nullable元素为项目设置可为空的注释上下文和可为空的警告上下文
csproj。该元素配置编译器如何解释类型的可空性以及生成什么警告。有效设置为:
- enable:启用可空注释上下文。可空警告上下文已启用。引用类型的变量(例如字符串)是不可为空的。启用所有可空性警告。
- disable:可空注释上下文已禁用。可为空的警告上下文已禁用。引用类型的变量是忽略的,就像C#的早期版本一样。所有可空性警告均已禁用。
- safeonly:启用了可为空的注释上下文。可为空的警告上下文仅是安全的。引用类型的变量不可为空。启用所有安全性为空的警告。
- 警告:可空注释上下文已禁用。可空警告上下文已启用。引用类型的变量是忽略的。启用所有可空性警告。
- safeonlywarnings:可空注释上下文已禁用。可为空的警告上下文仅是安全的。引用类型的变量是忽略的。启用所有安全性为空的警告。
使用与可为空的值类型相同的语法来记录可为空的引用类型:将a
?附加到变量的类型。
用于调试和修复迭代器中的空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 THISpublic 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 THISpublic 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调用when时,将立即
GetFrobsForReal执行空检查,然后在序列迭代时执行。
如果您检查
LINQ对象的参考源,您会发现整个使用了这种技术。编写起来有点笨拙,但是它使调试null错误更加容易。
优化代码以方便调用者,而不是作者 。
关于不安全代码中的空取消引用的说明
C#顾名思义,它具有“不安全”模式,这是非常危险的,因为没有强制执行提供内存安全和类型安全的常规安全机制。
除非您对内存的工作原理有透彻和深入的了解,否则不要编写不安全的代码 。
在不安全模式下,您应该意识到两个重要事实:
- 取消引用空 指针* 会产生与取消引用空 引用 相同的异常 *
- 在某些情况下,取消引用无效的非null指针 可能会 产生该异常
要了解为什么会这样,它有助于首先了解.NET如何产生null取消引用异常。(这些详细信息适用于Windows上运行的.NET;其他操作系统使用类似的机制。)
内存被虚拟化
Windows;每个进程都会获得由操作系统跟踪的许多“页面”内存的虚拟内存空间。内存的每个页面上都设置了标志,这些标志确定如何使用它:读取,写入,执行等。该
最低 页面被标记为“产生的任何方式,如果使用过的错误”。
空指针和in
C#中的空引用都在内部表示为数字零,因此任何尝试将其取消引用到其相应的内存中的操作都会导致操作系统产生错误。然后,.NET运行时将检测到此错误,并将其转变为null取消引用异常。
这就是为什么同时取消引用空指针和空引用会产生相同的异常的原因。
那第二点呢?取消引用落在虚拟内存最低页中的 任何 无效指针会导致相同的操作系统错误,从而导致相同的异常。
为什么这有意义?好吧,假设我们有一个包含两个int的结构,以及一个等于null的非托管指针。如果尝试取消引用结构中的第二个int,
CLR则不会尝试访问零位置的存储;它将访问第四位置的存储。但从逻辑上讲,这是一个空取消引用,因为我们要
通过 空到达该地址。
如果您使用的是不安全的代码,并且会收到null解除引用异常,则请注意,有问题的指针不必为null。它可以在最低页面的任何位置,并且将产生此异常。



