c# – 什么是NullReferenceException,我该如何解决?
我有一些代码,当它执行时,它会抛出一个NullReferenceException,说:
这是什么意思,我该怎么做才能解决这个错误? 解决方法
原因是什么?
底线 您正在尝试使用null(或VB.NET中的Nothing).这意味着您要么将其设置为null,要么根本不将其设置为任何内容. 像其他任何东西一样,null被传递.如果方法“A”中为null,则可能是方法“B”将null传递给方法“A”. null可以有不同的含义: >未初始化且因此无效的对象变量.在这种情况下,如果访问此类对象的属性或方法,则会导致NullReferenceException. 本文的其余部分将详细介绍并显示许多程序员经常犯的错误,这些错误可能导致NullReferenceException. 进一步来说 抛出NullReferenceException的运行时总是意味着同样的事情:您正在尝试使用引用,并且引用未初始化(或者它曾被初始化,但不再初始化). 这意味着引用为null,并且您无法通过空引用访问成员(例如方法).最简单的情况: string foo = null; foo.ToUpper(); 这将在第二行抛出NullReferenceException,因为您无法在指向null的字符串引用上调用实例方法ToUpper(). 调试 你如何找到NullReferenceException的来源?除了查看将在其发生的位置准确抛出的异常本身之外,Visual Studio中调试的一般规则适用:放置战略断点和inspect your variables,将鼠标悬停在其名称上,打开(快速)观察窗口或使用各种调试面板,如本地和汽车. 如果要查找引用的位置或未设置引用,请右键单击其名称并选择“查找所有引用”.然后,您可以在每个找到的位置放置一个断点,并在附加调试器的情况下运行程序.每次调试器在这样的断点上中断时,您需要确定是否期望引用为非null,检查变量并验证它是否指向实例. 通过这种方式跟踪程序流,您可以找到实例不应为null的位置,以及未正确设置的原因. 例子 可以抛出异常的一些常见场景: 通用 ref1.ref2.ref3.member 如果ref1或ref2或ref3为null,那么您将获得NullReferenceException.如果你想解决这个问题,那么通过将表达式重写为更简单的等价物来找出哪一个是null: 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)null引用,可以在父(Book)对象的构造函数中初始化它. 嵌套对象初始化器 这同样适用于嵌套对象初始值设定项: Book b1 = new Book { Author = { Age = 45 } }; 这转化为 Book b1 = new Book(); b1.Author.Age = 45; 使用new关键字时,它只创建Book的新实例,但不创建Person的新实例,因此作者属性仍然为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" }); 新Person仅创建Person的实例,但Books集合仍为null.集合初始值设定项语法不会创建集合 排列 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.从控制器返回空模型(或模型属性)时,视图访问它时会发生异常: // 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中声明的顺序(即,在comboBox1之前列出label1,忽略设计哲学问题)至少会解决NullReferenceException. 与as一起演员 var myThing = someObject as Thing; 这不会抛出InvalidCastException,但在转换失败时返回null(当someObject本身为null时).所以要注意这一点. LINQ FirstOrDefault()和SingleOrDefault() 普通版本First()和Single()在没有任何内容时抛出异常.在这种情况下,“OrDefault”版本返回null.所以要注意这一点. 的foreach 当您尝试迭代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,则可以在访问实例成员之前检查它是否为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; } 如果值永远不为null,请使用Debug.Assert,以便在发生异常之前捕获问题. 当您在开发期间知道方法可能,但永远不应该返回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,导致它在发布模式下在运行时book == null时再次抛出NullReferenceException. 对可空值类型使用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 使用null合并运算符:?? [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; } 使用null条件运算符:?.或?[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,并且如果person.Title为null,则不会调用ToUpper. 当然,您仍然需要检查标题为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,您可以安全地检查它.如果它包含一个数组,它将执行相同的操作: 在迭代器中调试和修复null derefs的特殊技术 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) { ... } 如果无效的结果为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#具有“不安全”模式,顾名思义,这种模式极其危险,因为不强制执行提供存储器安全性和类型安全性的正常安全机制.除非您对内存的工作原理有深入的了解,否则不应编写不安全的代码. 在不安全模式下,您应该了解两个重要事实: >取消引用空指针会产生与取消引用空引用相同的异常 要理解为什么会这样,有助于理解.NET首先如何产生空解除引用异常. (这些细节适用于在Windows上运行的.NET;其他操作系统使用类似的机制.) 内存在Windows中虚拟化;每个进程获得操作系统跟踪的许多“页面”内存的虚拟内存空间.每页内存都设置了标志,用于确定如何使用它:读取,写入,执行等.最低页面标记为“如果以任何方式使用会产生错误”. C#中的空指针和空引用都在内部表示为数字零,因此任何将其取消引用到其相应的内存存储中的尝试都会导致操作系统产生错误.然后,.NET运行时检测到此错误并将其转换为空解除引用异常. 这就是为什么解除引用空指针和空引用会产生相同的异常. 第二点怎么样?取消引用位于虚拟内存最低页面的任何无效指针会导致相同的操作系统错误,从而导致相同的异常. 为什么这有意义?好吧,假设我们有一个包含两个int的结构,一个等于null的非托管指针.如果我们尝试取消引用结构中的第二个int,CLR将不会尝试访问零位置的存储;它将访问位置四的存储.但从逻辑上讲,这是一个空取消引用,因为我们通过null到达该地址. 如果您正在处理不安全的代码并且您获得了空解除引用异常,请注意违规指针不必为空.它可以是最低页面中的任何位置,并且将生成此异常. (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |