什么是NullReferenceException以及如何修复它

什么是NullReferenceException以及如何修复它

技术背景

在.NET开发中,NullReferenceException是一种常见的异常。当试图使用一个值为null(在VB.NET中为Nothing)的引用时,就会抛出该异常。这意味着要么将引用显式设置为null,要么根本没有对其进行初始化。NullReferenceException的出现会导致程序崩溃,影响程序的稳定性和可靠性,因此了解其产生原因和修复方法至关重要。

实现步骤

查找异常源

  • 查看异常信息:异常会在其发生的精确位置抛出,查看异常本身能获取关键信息。
  • 使用调试工具:在Visual Studio中,可通过设置战略断点,使用调试窗口(如QuickWatch、Locals和Autos)来检查变量的值。
  • 查找引用设置位置:右键单击引用名称,选择“Find All References”,在找到的每个位置设置断点,运行程序并在断点处检查引用是否为非null

常见场景及解决方法

通用链式引用

1
ref1.ref2.ref3.member

ref1ref2ref3null,会抛出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; // 未初始化Author属性
}
}

可在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]; // numbers为null

要先初始化数组。

数组元素

1
2
Person[] people = new Person[5];
people[0].Age = 20; // people[0]为null

需为数组元素逐个创建实例。

交错数组

1
2
long[][] array = new long[1][];
array[0][0] = 3; // 仅初始化了第一维

应先初始化内部数组。

集合/列表/字典

1
2
Dictionary<string, int> agesForNames = null;
int age = agesForNames["Bob"]; // agesForNames为null

先初始化集合。

范围变量(间接/延迟)

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
// 若"FirstName"会话值未设置,会抛出异常
string firstName = Session["FirstName"].ToString();

在使用会话值前检查是否为null

ASP.NET MVC空视图模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Controller
public class Restaurant:Controller
{
public ActionResult Search()
{
return View(); // 忘记提供模型
}
}

// Razor视图
@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); // 可能返回null
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) 
{
// 知道此方法不应返回null
var book = library.GetBook(knownBookID);

// 若为null,在这一行抛出异常
Debug.Assert(book != null, "Library didn't return a book for known book ID.");

// 其他代码

return book.Title; // 在调试模式下不会抛出NullReferenceException
}

使用GetValueOrDefault()

1
2
3
4
5
6
7
DateTime? appointment = null;
Console.WriteLine(appointment.GetValueOrDefault(DateTime.Now));
// 因appointment为null,将显示默认值

appointment = new DateTime(2022, 10, 20);
Console.WriteLine(appointment.GetValueOrDefault(DateTime.Now));
// 将显示appointment日期,而非默认值

使用空合并运算符??

1
2
3
4
5
6
7
IService CreateService(ILogger log, Int32? frobPowerLevel)
{
var serviceImpl = new MyService(log ?? NullLog.Instance);

// 也可使用空合并运算符重写GetValueOrDefault()
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。需使用线程同步机制确保线程安全。


什么是NullReferenceException以及如何修复它
https://119291.xyz/posts/what-is-nullreferenceexception-and-how-to-fix-it/
作者
ww
发布于
2025年5月26日
许可协议