什么是NullReferenceException以及如何修复它 技术背景 在.NET开发中,NullReferenceException
是一种常见的异常。当试图使用一个值为null
(在VB.NET中为Nothing
)的引用时,就会抛出该异常。这意味着要么将引用显式设置为null
,要么根本没有对其进行初始化。NullReferenceException
的出现会导致程序崩溃,影响程序的稳定性和可靠性,因此了解其产生原因和修复方法至关重要。
实现步骤 查找异常源 查看异常信息 :异常会在其发生的精确位置抛出,查看异常本身能获取关键信息。使用调试工具 :在Visual Studio中,可通过设置战略断点,使用调试窗口(如QuickWatch、Locals和Autos)来检查变量的值。查找引用设置位置 :右键单击引用名称,选择“Find All References”,在找到的每个位置设置断点,运行程序并在断点处检查引用是否为非null
。常见场景及解决方法 通用链式引用 若ref1
、ref2
或ref3
为null
,会抛出NullReferenceException
。可将表达式拆分为简单形式来找出为null
的引用:
1 2 3 4 var r1 = ref1;var r2 = r1.ref2;var r3 = r2.ref3; r3.member
间接引用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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; } }
可在Book
类的构造函数中初始化Author
属性。
嵌套对象初始化器 1 2 3 4 Book b1 = new Book { Author = { Age = 45 } };
这实际上只创建了Book
的实例,Author
属性仍为null
。正确做法是显式创建Person
实例。
嵌套集合初始化器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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
实例,Books
集合为null
。应先初始化Books
集合。
数组 1 2 int [] numbers = null ;int n = numbers[0 ];
要先初始化数组。
数组元素 1 2 Person[] people = new Person[5 ]; people[0 ].Age = 20 ;
需为数组元素逐个创建实例。
交错数组 1 2 long [][] array = new long [1 ][]; array[0 ][0 ] = 3 ;
应先初始化内部数组。
集合/列表/字典 1 2 Dictionary<string , int > agesForNames = null ;int age = agesForNames["Bob" ];
先初始化集合。
范围变量(间接/延迟) 1 2 3 4 5 6 7 8 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();
避免向集合中添加null
元素。
事件(C#) 1 2 3 4 5 6 7 8 9 public class Demo { public event EventHandler StateChanged; protected virtual void OnStateChanged (EventArgs e ) { StateChanged(this , e); } }
在触发事件前检查事件是否为null
。
不良命名约定 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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); } }
使用规范命名,如为字段添加下划线前缀。
ASP.NET页面生命周期 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public partial class Issues_Edit : System.Web.UI.Page { protected TestIssue myIssue; protected void Page_Load (object sender, EventArgs e ) { if (!IsPostBack) { myIssue = new TestIssue(); } } protected void SaveButton_Click (object sender, EventArgs e ) { myIssue.Entry = "NullReferenceException here!" ; } }
确保在需要时正确初始化对象。
ASP.NET会话值 1 2 string firstName = Session["FirstName" ].ToString();
在使用会话值前检查是否为null
。
ASP.NET MVC空视图模型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Restaurant :Controller { public ActionResult Search () { return View(); } } @foreach (var restaurantSearch in Model.RestaurantSearch) { } <p>@Model.somePropertyName</p> <!-- 也会抛出异常 -->
在控制器中正确返回模型。
WPF控件创建顺序和事件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <Grid> <!-- Combobox先声明 --> <ComboBox Name="comboBox1" Margin="10" SelectedIndex="0" SelectionChanged="comboBox1_SelectionChanged"> <ComboBoxItem Content="Item 1" /> <ComboBoxItem Content="Item 2" /> <ComboBoxItem Content="Item 3" /> </ComboBox> <!-- Label后声明 --> <Label Name="label1" Content="Label" Margin="10" /> </Grid>
1 2 3 4 private void comboBox1_SelectionChanged (object sender, SelectionChangedEventArgs e ) { label1.Content = comboBox1.SelectedIndex.ToString(); }
调整控件声明顺序或在事件处理程序中检查控件是否已创建。
使用as
进行类型转换 1 var myThing = someObject as Thing;
转换失败时返回null
,使用前需检查。
LINQ的FirstOrDefault()
和SingleOrDefault()
这两个方法在没有匹配项时返回null
,使用时要注意。
foreach
循环1 2 List<int > list = null ; foreach (var v in list) { }
确保集合不为null
。
核心代码 显式检查null
并忽略 1 2 3 4 5 6 7 void PrintName (Person p ) { if (p != null ) { Console.WriteLine(p.Name); } }
显式检查null
并提供默认值 1 2 3 4 5 6 string GetCategory (Book b ) { if (b == null ) return "Unknown" ; return b.Category; }
显式检查null
并抛出自定义异常 1 2 3 4 5 6 7 string GetCategory (string bookTitle ) { var book = library.FindBook(bookTitle); if (book == null ) throw new BookNotFoundException(bookTitle); return book.Category; }
使用Debug.Assert
1 2 3 4 5 6 7 8 9 10 11 12 string GetTitle (int knownBookID ) { var book = library.GetBook(knownBookID); Debug.Assert(book != null , "Library didn't return a book for known book ID." ); return book.Title; }
使用GetValueOrDefault()
1 2 3 4 5 6 7 DateTime? appointment = null ; Console.WriteLine(appointment.GetValueOrDefault(DateTime.Now)); appointment = new DateTime(2022 , 10 , 20 ); Console.WriteLine(appointment.GetValueOrDefault(DateTime.Now));
使用空合并运算符??
1 2 3 4 5 6 7 IService CreateService (ILogger log, Int32? frobPowerLevel ) { var serviceImpl = new MyService(log ?? NullLog.Instance); serviceImpl.FrobPowerLevel = frobPowerLevel ?? 5 ; }
使用空条件运算符?.
1 var title = person.Title?.ToUpper();
使用空上下文(C# 8) 在csproj
文件中设置Nullable
元素,配置编译器对类型可空性的解释和警告生成。
最佳实践 遵循命名规范 :使用一致的命名规范,避免因命名冲突导致未初始化的引用。尽早检查null
:在方法开始处检查输入参数是否为null
,并在必要时抛出ArgumentNullException
。使用工具辅助 :利用Resharper等工具,在编码过程中发现潜在的null
引用问题。结合设计模式 :采用设计模式,如依赖注入,确保对象正确初始化。测试覆盖 :编写全面的单元测试,覆盖可能出现NullReferenceException
的场景。常见问题 调试时难以定位问题 在复杂的代码中,NullReferenceException
可能在远离问题根源的地方抛出。可使用调试工具逐步跟踪程序执行流程,检查变量的值,找出未初始化的引用。
忽略编译器警告 编译器警告可能提示潜在的null
引用问题,应重视并及时处理。
空Try/Catch
块隐藏问题 空的Try/Catch
块会隐藏异常信息,导致难以定位问题。应让异常抛出,以便及时发现和修复问题。
多线程环境下的问题 在多线程环境中,对共享对象的并发访问可能导致NullReferenceException
。需使用线程同步机制确保线程安全。