从 == null 到 is null 的演进
在 C# 编程中,null 检查是日常开发中最常见的操作之一。从 C# 1.0 到 C# 6.0,开发者们一直使用 == null 进行 null 检查,这种方式简单直观,但在某些场景下存在潜在问题。C# 7.0 引入了模式匹配(Pattern Matching)特性,其中 is null 作为一种新的 null 检查方式应运而生。随后在 C# 9.0 中,又引入了 is not null 语法,进一步完善了 null 检查的模式匹配体系。
本文将深入剖析 is null 的设计原理、底层实现、与传统 == null 的核心差异,以及在不同场景下的最佳实践,帮助你全面掌握这一现代 C# 特性。
一、is null 的本质:模式匹配中的 null 模式
1.1 模式匹配基础
模式匹配是 C# 7.0 引入的核心语言特性,它允许开发者基于值的形状、类型或其他属性来检查值并提取信息。is null 是模式匹配中的常量模式(Constant Pattern) 特例,专门用于检查值是否为 null 引用或 null 值。
1.2 语法与基本用法
is null 的基本语法非常简洁:
csharp
if (variable is null)
{
// 处理 null 情况
}
对于可空值类型,is null 同样适用:
csharp
int? nullableInt = null;
if (nullableInt is null)
{
Console.WriteLine("nullableInt is null");
}
C# 9.0 进一步引入了 is not null 语法,用于检查非 null 情况:
csharp
if (variable is not null)
{
// 处理非 null 情况
}
二、is null 与 == null 的核心差异
2.1 运算符重载的影响(最关键差异)
这是两种写法最本质的区别,也是微软官方推荐使用 is null 的核心原因。
| 写法 | 运算符重载行为 | 底层实现 |
|---|---|---|
variable == null |
可能调用用户自定义的 == 运算符 |
依赖类型的 == 实现,可能是值比较或引用比较 |
variable is null |
编译器保证不调用任何用户重载的 == 或 != 运算符 |
等价于 object.ReferenceEquals(variable, null),直接比较内存地址 |
示例:重载 == 运算符时的行为差异
csharp
public class CustomType
{
public int Id { get; }
public CustomType(int id) => Id = id;
// 自定义 == 运算符,让所有实例都"等于"null
public static bool operator ==(CustomType a, CustomType b) => true;
public static bool operator !=(CustomType a, CustomType b) => false;
public override bool Equals(object obj) => true;
public override int GetHashCode() => 0;
}
// 测试代码
var instance = new CustomType(1);
Console.WriteLine(instance == null); // 输出 true(调用自定义 == 运算符)
Console.WriteLine(instance is null); // 输出 false(执行引用相等性检查)
在这个例子中,== null 的结果被自定义运算符扭曲,而 is null 始终保持正确的 null 检查行为,不受类型实现影响。
2.2 可空值类型处理
对于可空值类型(如 int?、string?),两种写法都能正确处理,但 is null 提供了更统一的语法体验:
csharp
int? num1 = null;
int? num2 = 42;
// == null 方式
Console.WriteLine(num1 == null); // true
Console.WriteLine(num2 == null); // false
// is null 方式
Console.WriteLine(num1 is null); // true
Console.WriteLine(num2 is null); // false
2.3 与模式匹配的集成
is null 最大的优势之一是与其他模式匹配特性的无缝集成,这是 == null 无法比拟的:
csharp
// 结合类型模式进行 null 检查和类型转换(C# 7.0+)
if (input is string str && str.Length > 0)
{
Console.WriteLine($"Valid string: {str}");
}
// 结合属性模式(C# 8.0+)
if (input is Person { Name: not null } person)
{
Console.WriteLine($"Person name: {person.Name}");
}
// 结合 switch 表达式(C# 8.0+)
var result = input switch
{
null => "Value is null",
int i => $"Integer: {i}",
string s => $"String: {s}",
_ => "Unknown type"
};
三、is null 的底层实现与性能分析
3.1 编译期处理
C# 编译器对 is null 有特殊处理,确保其行为不受用户代码影响:
- 对于引用类型,
is null编译为object.ReferenceEquals(variable, null) - 对于可空值类型,
is null编译为检查HasValue属性是否为false - 对于非可空值类型,
is null编译为常量false,并给出编译警告
3.2 IL 代码对比
让我们通过一个简单的例子对比 is null 和 == null 的 IL 代码:
C# 代码:
csharp
public void CheckNull(string input)
{
if (input == null) { }
if (input is null) { }
}
IL 代码(简化版):
il
// input == null
ldarg.1 // 加载 input
ldnull // 加载 null
ceq // 比较相等(可能调用重载的 ==)
brfalse.s // 如果不等,跳转到下一个检查
// input is null
ldarg.1 // 加载 input
ldnull // 加载 null
call bool [System.Runtime]System.Object::ReferenceEquals(object, object) // 直接调用 ReferenceEquals
brfalse.s // 如果不等,跳转到方法结束
可以看到,is null 直接调用 object.ReferenceEquals,而 == null 使用 ceq 指令,可能被重载的 == 运算符影响。
3.3 性能考量
在大多数场景下,is null 和 == null 的性能差异可以忽略不计,因为 JIT 编译器会进行优化。但在以下两种情况下,is null 可能更有优势:
- 类型重载了
==运算符 :is null避免了方法调用,直接进行引用比较,性能略优 - 模式匹配组合场景 :
is null可以与其他模式组合使用,减少代码行数,提高可读性,间接提升开发效率
四、is not null 与其他非 null 检查方式对比
C# 9.0 引入的 is not null 为非 null 检查提供了更流畅的语法,以下是几种常见非 null 检查方式的对比:
| 写法 | 适用版本 | 运算符重载影响 | 可读性 | 推荐指数 |
|---|---|---|---|---|
if (variable != null) |
所有版本 | 可能调用重载的 != 运算符 |
⭐⭐⭐ | ⭐⭐⭐ |
if (!(variable is null)) |
C# 7.0+ | 不受重载影响 | ⭐⭐ | ⭐⭐⭐⭐ |
if (variable is not null) |
C# 9.0+ | 不受重载影响 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
if (variable is { }) |
C# 8.0+ | 不受重载影响 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
is not null 的优势:
- 语法简洁直观,符合自然语言表达习惯
- 不受运算符重载影响,行为更可靠
- 可与其他模式组合使用,如
if (input is not null and string str)
五、微软官方建议与代码分析规则
5.1 IDE0041 代码分析规则
微软在 .NET 代码分析器中提供了 IDE0041 规则,明确推荐使用 is null 进行 null 检查,而非 == null 或 object.ReferenceEquals。
该规则的默认配置为:
json
{
"dotnet_style_prefer_is_null_check_over_reference_equality_method": true,
"dotnet_style_prefer_is_null_check_over_equality_operator": true
}
5.2 官方文档明确说明
微软官方文档指出:
编译器保证在评估
x is null时,不会调用任何用户重载的相等运算符==。这使得is null成为检查 null 引用的可靠方式,不受类型实现的影响。
5.3 适用场景优先级
微软建议在以下场景优先使用 is null:
- 框架/库代码:确保 null 检查行为不受用户类型重载影响
- 公共 API 边界:提供一致、可靠的 null 检查行为
- 模式匹配组合场景:与其他模式(如类型模式、属性模式)结合使用
- 可空引用类型(NRT)场景:与 C# 8.0+ 的可空引用类型特性更好地配合
六、迁移指南:从 == null 到 is null
6.1 安全迁移的场景
大多数情况下,你可以安全地将 == null 替换为 is null,包括:
- 引用类型的 null 检查
- 可空值类型的 null 检查
- 不需要依赖
==运算符重载行为的场景
6.2 需要谨慎的场景
以下场景中,不应盲目迁移:
- 依赖自定义
==运算符行为的代码(罕见情况) - 非可空值类型 的 null 检查(虽然两种写法都会给出警告,但
is null更明确)
6.3 迁移步骤
- 启用 IDE0041 规则 :在项目的
.editorconfig文件中配置相关规则 - 使用 Visual Studio 重构工具 :右键点击
== null表达式,选择"快速操作和重构"→"使用 'is null' 检查" - 手动检查特殊情况 :对于重载了
==运算符的类型,确认迁移后行为是否符合预期 - 测试验证:确保迁移后的代码通过所有单元测试
6.4 迁移示例
重构前:
csharp
public string GetName(Person person)
{
if (person == null)
throw new ArgumentNullException(nameof(person));
return person.Name;
}
重构后:
csharp
public string GetName(Person person)
{
if (person is null)
throw new ArgumentNullException(nameof(person));
return person.Name;
}
七、高级用法:is null 与模式匹配的深度集成
7.1 结合类型模式进行 null 检查与转换
C# 7.0+ 允许在 is 表达式中同时进行 null 检查和类型转换,这是 == null 无法实现的:
csharp
// 安全地检查并转换为 string 类型
if (input is string str && str is not null && str.Length > 0)
{
Console.WriteLine($"Valid string: {str}");
}
// C# 9.0+ 更简洁的写法
if (input is string { Length: > 0 } str)
{
Console.WriteLine($"Valid string: {str}");
}
7.2 在 switch 语句/表达式中使用
is null 可以自然地与 switch 语句/表达式结合,实现更清晰的分支逻辑:
csharp
// switch 语句
switch (input)
{
case null:
Console.WriteLine("Input is null");
break;
case int i:
Console.WriteLine($"Integer: {i}");
break;
case string s when s.Length > 0:
Console.WriteLine($"Non-empty string: {s}");
break;
default:
Console.WriteLine("Unknown type");
break;
}
// C# 8.0+ switch 表达式
var description = input switch
{
null => "Null value",
int i => $"Integer: {i}",
string s => s.Length > 0 ? $"Non-empty string: {s}" : "Empty string",
_ => "Unknown type"
};
7.3 与可空引用类型的配合
C# 8.0 引入的可空引用类型(Nullable Reference Types, NRT)特性与 is null 完美配合,提供更强大的静态代码分析:
csharp
#nullable enable
public void Process(string? input)
{
if (input is not null)
{
// 在此块中,input 被编译器视为非 null 引用
Console.WriteLine(input.Length); // 无警告
}
Console.WriteLine(input.Length); // 编译警告:可能为 null
}
八、常见误区与最佳实践
8.1 常见误区
- 认为
is null与== null完全等价 :在类型重载==运算符时,两者行为可能完全不同 - 在非可空值类型上使用
is null:虽然合法,但会给出编译警告,且结果恒为false - 过度使用
is null而忽略其他模式 :is { }有时更适合非 null 检查,尤其是需要同时创建变量时
8.2 最佳实践
- 优先使用
is null和is not null:遵循微软官方建议,提高代码的可靠性和一致性 - 结合模式匹配简化代码 :在需要同时进行类型检查和 null 检查时,使用
is Type variable语法 - 在框架/库代码中强制使用
is null:确保 null 检查行为不受用户类型影响 - 使用
is not null替代!(is null):C# 9.0+ 中,is not null语法更简洁、可读性更高 - 与可空引用类型特性配合使用 :启用 NRT 后,
is null/is not null能帮助编译器提供更准确的 null 分析
九、总结:为什么 is null 是现代 C# 的首选
is null 不仅仅是一种语法糖,它代表了 C# 语言向更安全、更具表达力方向的演进。相比传统的 == null,is null 具有以下核心优势:
- 行为可靠 :不受类型重载的
==运算符影响,始终执行引用相等性检查 - 语法统一:与模式匹配特性无缝集成,支持更复杂的检查逻辑
- 官方推荐 :微软通过 IDE0041 规则明确推荐使用
is null - 未来兼容 :与 C# 后续版本的新特性(如
is not null、属性模式等)更好地配合
在 C# 7.0+ 的项目中,我们建议全面采用 is null 进行 null 检查,让代码更具现代感、更可靠、更易维护。