原因是什么?
底线
你正在尝试使用的东西是null
(or Nothing
在 VB.NET 中)。这意味着您可以将其设置为null
,或者您根本没有将其设置为任何内容。
就像其他任何事情一样,null
被传递。如果是null
in方法“A”,可能是方法“B”通过了null
to方法“A”。
null
可以有不同的含义:
- 对象变量是未初始化的因此没有指向任何内容。在这种情况下,如果您访问此类对象的成员,则会导致
NullReferenceException
.
- 开发商是using
null
故意表明没有可用的有意义的值。请注意,C# 具有变量可为空数据类型的概念(例如数据库表可以有可为空字段) - 您可以分配null
例如,向他们表明其中没有存储任何值int? a = null;
(这是一个快捷方式Nullable<int> a = null;
) 其中问号表示允许存储null
在变量中a
。您可以使用以下命令进行检查if (a.HasValue) {...}
或与if (a==null) {...}
。可空变量,例如a
在这个例子中,允许通过访问该值a.Value
明确地,或者像平常一样通过a
.
Note通过访问它a.Value
抛出一个InvalidOperationException
代替NullReferenceException
if a
is null
- 你应该事先进行检查,即如果你有另一个不可为空的变量int b;
那么你应该做这样的作业if (a.HasValue) { b = a.Value; }
或更短if (a != null) { b = a; }
.
本文的其余部分将更详细地介绍许多程序员经常犯的错误,这些错误可能会导致NullReferenceException
.
进一步来说
The runtime
扔一个NullReferenceException
always意味着同样的事情:您正在尝试使用引用,并且该引用未初始化(或者它是once已初始化,但是是不再已初始化)。
这意味着参考是null
,并且您无法通过null
参考。最简单的情况:
string foo = null;
foo.ToUpper();
这会抛出一个NullReferenceException
在第二行,因为你不能调用实例方法ToUpper()
on a string
参考指向null
.
调试
你如何找到一个的来源NullReferenceException
?除了查看异常本身(异常将在异常发生的位置准确抛出)之外,Visual Studio 中的调试一般规则也适用:放置策略断点并检查你的变量,通过将鼠标悬停在其名称上、打开(快速)监视窗口或使用各种调试面板(例如“本地”和“自动”)来实现。
如果您想找出引用的设置或未设置的位置,请右键单击其名称并选择“查找所有引用”。然后,您可以在每个找到的位置放置一个断点,并使用附加的调试器运行程序。每次调试器在此类断点处中断时,您都需要确定是否希望引用为非空,检查变量,并验证它是否在您希望时指向实例。
通过这种方式遵循程序流程,您可以找到实例不应该为空的位置,以及为什么没有正确设置它。
Examples
一些可能引发异常的常见场景:
Generic
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
, the HttpContext.Current
可以为 null,或者User
属性可以为 null,或者Identity
属性可能为空。
Indirect
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" });
The new Person
只创建一个实例Person
,但是Books
收藏还在null
。收藏品Initializer
语法不创建集合
为了p1.Books
,它仅翻译为p1.Books.Add(...)
声明。
Array
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 编译器插入了对事件使用情况的空检查,因此无需检查事件Nothing
在 VB.NET 中。)
错误的命名约定:
如果您对字段的命名与本地字段的命名不同,您可能会意识到您从未初始化过该字段。
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 空视图模型
如果引用属性时发生异常@Model
in an ASP.NET MVC View
,你需要明白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
按照它们在视觉树中出现的顺序。 ANullReferenceException
如果是带有事件处理程序等的早期创建的控件,则将在以下情况下引发: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>
Here comboBox1
之前创建的label1
. If comboBox1_SelectionChanged
尝试引用`label1,它尚未被创建。
private void comboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
label1.Content = comboBox1.SelectedIndex.ToString(); // NullReferenceException here!!
}
更改声明中的顺序XAML
(即列出label1
before comboBox1
,忽略设计理念的问题)至少可以解决NullReferenceException
here.
演员阵容as
var myThing = someObject as Thing;
这不会引发InvalidCastException
但返回一个null
当强制转换失败时(以及当someObject
本身为空)。所以要注意这一点。
LINQ FirstOrDefault()
and SingleOrDefault()
普通版本First()
and Single()
当什么都没有的时候抛出异常。 “OrDefault”版本返回null
在这种情况下。所以要注意这一点。
foreach
foreach
当你尝试迭代 a 时抛出null
收藏。通常是由意外引起的null
来自返回集合的方法的结果。
List<int> list = null;
foreach(var v in list) { } // NullReferenceException here
更实际的示例 - 从 XML 文档中选择节点。如果未找到节点但初始调试显示所有属性有效,则会抛出异常:
foreach (var node in myData.MyXml.DocumentNode.SelectNodes("//Data"))
避免的方法
明确检查null
并忽略null
values.
如果您希望参考有时是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;
}
Use 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.
}
虽然这个检查不会出现在您的发布版本中,导致它抛出NullReferenceException
再次当book == null
在运行时处于发布模式。
Use GetValueOrDefault()
for 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].
当 a 时提供默认值的简写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;
}
使用空条件运算符:?.
or ?[x]
对于数组(在 C# 6 和 VB.NET 14 中可用):
这有时也称为安全导航或 Elvis(以其形状)操作员。如果运算符左侧的表达式为 null,则不会计算右侧,而是返回 null。这意味着像这样的情况:
var title = person.Title.ToUpper();
如果此人没有头衔,这将引发异常,因为它正在尝试调用ToUpper
具有空值的属性。
In C# 5
下面,这可以通过以下方式来保护:
var title = person.Title == null ? null : person.Title.ToUpper();
现在 title 变量将为 null 而不是抛出异常。 C# 6 为此引入了更短的语法:
var title = person.Title?.ToUpper();
这将导致标题变量为null
,并调用ToUpper
不成立,如果person.Title
is null
.
当然,你still必须检查title
for 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");
This will do the following: If myIntArray
is null
, the expression returns null
and you can safely check it. If it contains an array, it will do the same as:
elem = myIntArray[i];
and returns the ith element.
使用空上下文(C# 8 中可用):
引入于C# 8
、 null 上下文和可为 null 的引用类型对变量执行静态分析,并在某个值可能是潜在的值时提供编译器警告null
或已设置为null
。可空引用类型允许显式允许类型null
.
可以使用以下命令为项目设置可为空注释上下文和可为空警告上下文Nullable
你的元素csproj
文件。此元素配置编译器如何解释类型的可为空性以及生成哪些警告。有效设置为:
-
enable
:可空注释上下文已启用。可以为空的警告上下文已启用。引用类型的变量(例如字符串)是不可为空的。所有可空性警告均已启用。
-
disable
:可空注释上下文已禁用。可为空的警告上下文已禁用。引用类型的变量是不可见的,就像 C# 的早期版本一样。所有可空性警告均被禁用。
-
safeonly
:可空注释上下文已启用。可为空的警告上下文是安全的。引用类型的变量不可为 null。所有安全可空性警告均已启用。
-
warnings
:可空注释上下文已禁用。可以为空的警告上下文已启用。引用类型的变量是不可见的。所有可空性警告均已启用。
-
safeonlywarnings
:可空注释上下文已禁用。可为空的警告上下文是安全的。
引用类型的变量是不可见的。所有安全可空性警告均已启用。
可空引用类型使用与可空值类型相同的语法来表示:?
附加到变量的类型。
用于调试和修复迭代器中空解引用的特殊技术
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) { ... }
If whatever
结果是null
then 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();
}
为什么这是错误的?因为迭代器块实际上并不run直到foreach
!致电给GetFrobs
只是返回一个对象当迭代时将运行迭代器块。
通过写一个null
像这样检查你可以防止NullReferenceException
,但是你移动了NullArgumentException
到了这一点迭代,还没有到这个地步call, 那就是调试起来非常混乱.
正确的修复方法是:
// 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();
}
也就是说,创建一个具有迭代器块逻辑的私有帮助器方法和一个执行以下操作的公共表面方法null
检查并返回迭代器。现在,当GetFrobs
被称为,null
检查立即发生,然后GetFrobsForReal
当迭代序列时执行。
如果您检查参考源LINQ
对于对象,您将看到整个过程中都使用了这种技术。它写起来稍微有点笨拙,但它使调试无效错误变得更加容易。优化代码是为了调用者的方便,而不是为了作者的方便.
关于不安全代码中空取消引用的注释
C#
有一个“不安全”模式,顾名思义,这是极其危险的,因为提供内存安全和类型安全的正常安全机制没有得到强制执行。除非您对内存的工作原理有透彻而深入的了解,否则您不应该编写不安全的代码.
在不安全模式下,您应该注意两个重要事实:
- 取消引用 nullpointer产生与取消引用 null 相同的异常参考
- 取消引用无效的非空指针can在某些情况下产生该异常
要理解其中的原因,有助于理解 .NET 如何生成NullReferenceException
首先。 (这些详细信息适用于在 Windows 上运行的 .NET;其他操作系统使用类似的机制。)
内存被虚拟化为Windows
;每个进程都获得由操作系统跟踪的许多内存“页”组成的虚拟内存空间。内存的每个页面上都设置了标志,用于确定如何使用它:读取、写入、执行等。这lowest页面被标记为“如果以任何方式使用都会产生错误”。
空指针和空引用都在C#
内部表示为数字零,因此任何将其取消引用到其相应内存存储的尝试都会导致操作系统产生错误。然后.NET运行时检测到这个错误并将其转换为NullReferenceException
.
这就是为什么取消引用空指针和空引用会产生相同的异常。
那么第二点呢?解引用any落在虚拟内存最低页中的无效指针会导致相同的操作系统错误,从而导致相同的异常。
为什么这是有道理的?好吧,假设我们有一个包含两个 int 的结构,以及一个等于 null 的非托管指针。如果我们尝试取消引用结构中的第二个 int,CLR
不会尝试访问位置零处的存储;它将访问位置四的存储。但从逻辑上讲,这是一个空取消引用,因为我们正在访问该地址via零。
如果您正在使用不安全的代码并且您会得到NullReferenceException
,只需注意有问题的指针不必为空。可以是最底层页面的任意位置,都会产生该异常。