我有一些代码,当它执行时,抛出一个NullReferenceException
,说。
对象引用没有设置为一个对象的实例。
这是什么意思,我可以做什么来解决这个错误?
∮什么是原因?
你试图使用的东西是 "空"(或者VB.NET中的 "无")。这意味着你要么把它设置为 "null",要么你根本就没有把它设置为任何东西。
像其他东西一样,null
会被传来传去。如果在方法"A"中是 "null",可能是方法"B"将 "null "传递给方法"A"。
null
可以有不同的含义。
1.对象变量是未初始化的,因此没有指向任何东西。在这种情况下,如果你访问这种对象的属性或方法,就会引起一个 "NullReferenceException"。
2.2. 开发者故意使用 "null "来表示没有可用的有意义的值。请注意,C#有变量的可空数据类型的概念(就像数据库表可以有可空字段)--你可以给它们分配 "null "来表示没有存储的值,例如int? a = null;
其中的问号表示允许在变量a'中存储空。你可以用
if (a.HasValue) {...}或者用
if (a==null) {...}来检查。Nullable变量,比如本例中的
a',允许通过a.Value'明确地访问其值,或者像平常一样通过
a'访问。如果a'是
空'的话,Value'会抛出一个
无效操作异常',而不是空参考异常'--你应该事先进行检查,也就是说,如果你有另一个on-nullable变量
int b;,那么你应该做类似
if (a.HasValue) { b = a.Value; }的赋值或者更短的
if (a != null) { b = a; }`。
本文的其余部分将详细介绍,并展示许多程序员经常犯的错误,这些错误会导致 "NullReferenceException"。
运行时抛出的 "NullReferenceException "总是意味着同样的事情:你试图使用一个引用,而该引用没有被初始化(或者它曾经被初始化过,但现在不再被初始化)。 这意味着该引用是 "空 "的,你不能通过 "空 "的引用访问成员(如方法)。最简单的情况是。
string foo = null;
foo.ToUpper();
这将在第二行抛出一个NullReferenceException
,因为你不能在指向null
的string
引用上调用实例方法ToUpper()
。
你如何找到 "NullReferenceException "的来源?除了查看异常本身(它将准确地抛出在它发生的位置),Visual Studio中调试的一般规则也适用:放置战略断点和检查你的变量,可以通过将鼠标悬停在它们的名字上,打开一个(快速)观察窗口或使用各种调试面板,如本地和自动。 如果你想知道引用在哪里被设置或未被设置,右击其名称并选择"查找所有引用"。然后你可以在每一个找到的位置放置一个断点,并在连接调试器的情况下运行你的程序。每当调试器在这样的断点上中断时,你需要确定你期望的引用是否为非空,检查变量,并验证它在你期望的时候是否指向一个实例。 通过这样跟踪程序流程,你可以找到实例不应该为空的位置,以及为什么没有正确设置。
一些可以抛出异常的常见情况。
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
可能为空,或者User
属性可能为空,或者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 "这个属性仍然是 "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" });
novel 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 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
}
}
如果你对字段的命名与locals不同,你可能已经意识到你从未初始化过这个字段。
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;
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!";
}
}
// 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
的属性时发生异常,你需要理解的是,当你返回
视图时,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控件是在调用`InitializeComponent'时按照它们在可视化树中出现的顺序创建的。 如果早期创建的控件带有事件处理程序等,则会产生一个 "NullReferenceException"。在 "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>
这里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;
这不会抛出一个InvalidCastException,但是当投递失败时(以及当someObject本身为空时)会返回一个`null'。所以要注意这一点。
普通版本的First()
和Single()
在没有任何东西的时候会产生异常。在这种情况下,"OrDefault" 版本返回null。所以要注意这一点。
foreach
在你试图遍历空集合时抛出。通常是由于返回集合的方法产生了意外的`null'结果造成的。
List<int> list = null;
foreach(var v in list) { } // exception
更现实的例子--从XML文档中选择节点。如果没有找到节点就会抛出,但最初的调试显示所有属性都有效。
foreach (var node in myData.MyXml.DocumentNode.SelectNodes("//Data"))
如果你期望引用有时是空的,你可以在访问实例成员之前检查它是否为 "空"。
void PrintName(Person p) {
if (p != null) {
Console.WriteLine(p.Name);
}
}
你期望返回一个实例的方法调用可以返回`null',例如,当被寻找的对象找不到时。在这种情况下,你可以选择返回一个默认值。
string GetCategory(Book b) {
if (b == null)
return "Unknown";
return b.Category;
}
你也可以抛出一个自定义异常,只是在调用代码中捕获它。
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'。
GetValueOrDefault()
为可忽略的值类型提供一个默认值,当它们是`空'时。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中可用)。这有时也被称为安全导航或猫王(以其形状命名)操作符。如果操作符左边的表达式是空的,那么右边的表达式将不会被评估,而是返回空。这意味着像这样的情况。
var title = person.Title.ToUpper();
如果这个人没有头衔,这将抛出一个异常,因为它试图在一个空值的属性上调用ToUpper
。
在C# 5及以下版本中,这可以用以下方法来保护。
var title = person.Title == null ? null : person.Title.ToUpper();
现在标题变量将为空,而不是抛出一个异常。C# 6为此引入了一个更简短的语法。
var title = person.Title?.ToUpper();
这将导致标题变量为 "null",如果 "person.Title "为 "null",则不会调用 "ToUpper"。
当然,你还是要检查title
是否为空,或者使用空条件运算符和空凝聚运算符(??
)来提供一个默认值。
// 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为空,表达式返回空,你可以安全地检查它。如果它包含一个数组,它将做的事情和:
elem = myIntArray[i];
并返回ith元素。
在C#8中引入的null context's和nullable reference types对变量进行静态分析,如果一个值可能是null或者已经被设置为null,则提供编译器警告。nullable引用类型允许类型被明确地允许为空。 可以使用csproj文件中的Nullable元素为一个项目设置nullable注解上下文和nullable警告上下文。这个元素配置了编译器如何解释类型的可忽略性以及产生哪些警告。有效的设置是。
?
。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的参考资料,你会发现这个技术被贯穿使用。编写起来略显笨拙,但它使调试无效错误变得更加容易。 优化你的代码是为了方便调用者,而不是为了作者的方便。
C#有一个"不安全"模式,顾名思义,它是非常危险的,因为提供内存安全和类型安全的正常安全机制没有被执行。除非你对内存的工作原理有全面深入的了解,否则你不应该编写不安全的代码。 在不安全模式下,你应该注意到两个重要的事实。
这意味着有关的变量没有指向任何东西。我可以像这样生成。
SqlConnection connection = null;
connection.Open();
这就会产生错误,因为虽然我已经声明了变量"connection
",但它没有指向任何东西。当我试图调用成员"Open
"时,没有任何引用可以解决,因此会抛出错误。
为了避免这个错误。
1.在你试图对你的对象做任何事情之前,总是初始化你的对象。
2.2.如果你不确定对象是否为空,用object == null
来检查。
JetBrains'Resharper工具将识别你的代码中每一个有可能发生空引用错误的地方,让你加入空值检查。这个错误是错误的头号来源,IMHO。
这意味着你的代码使用了一个被设置为空的对象引用变量(即它没有引用一个实际的对象实例)。
为了防止这个错误,在使用可能为null的对象之前,应该对其进行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
}