从 .NET Framework 4.8 / C# 7.3 出发,进入现代 C# 的第一个大版本
可空值类型Nullable Value Types——C# 2.0 就有了:
int? score = null; // 语法糖,底层是 Nullable<int>
int result = score ?? -1; // null-coalescing(C# 2)
int result2 = score.HasValue ? score.Value : -1; // 等价
表达式体成员Expression-bodied Members——=> 语法糖,C# 6 引入方法/只读属性,C# 7 扩展到构造/析构/访问器:
public string FullName => $"{FirstName} {LastName}";
public Person(string name) => _name = name; // C# 7 新增
模式匹配Pattern Matching——C# 7 把类型检查 + 转换合为一步,C# 8 后大幅强化:
// C# 6:检查 + 转换两步
if (obj is string) { var s = (string)obj; ... }
// C# 7:一步到位
if (obj is string s && s.Length > 0) ...
关键转折:
string name = GetName();
int length = name.Length; // 💥 如果 name 是 null
// 编译完全通过,运行时爆炸
string? nullableValue; // ← 只对 value type 有效
// int? = Nullable<int>
// string? 不存在!(C# 7.3)
string name = GetName(); // 非 null 的承诺
int length = name.Length; // ✅ 编译器放心
string? maybeNull = GetName(); // 可以为 null
// int len = maybeNull.Length; // ⚠️ 编译器警告!
// int len = maybeNull!.Length // 我知道它不是 null(断言)
核心思想:引用类型Reference Types 的 null 不再是运行时Runtime问题,而变成编译时Compile-time问题。
用 TypeScript 类比:C# 8 的 NRT 就像 TypeScript 的 strictNullChecks——在编译期区分 string(非 null)和 string?(可能 null)。
在你的 .csproj 或文件顶部控制:
// 方式 1:项目级别(.csproj)
<Nullable>enable</Nullable>
// 方式 2:文件级别(放在文件最顶部)
#nullable enable
#nullable disable // 局部关闭
#nullable restore // 恢复到项目设置
<Nullable>enable</Nullable>。旧项目可以逐文件迁移。
| 写法 / Syntax | 含义 / Meaning | 编译器行为 / Compiler Behavior |
|---|---|---|
string name |
"这个引用不会为 null" | 如果你把它设为 null,警告 |
string? name |
"这个引用可以为 null" | 如果你直接访问它的成员,警告 |
name! |
"我断言它不是 null"(空包容操作符Null-forgiving Operator) | 抑制所有警告——相信自己,风险自负 |
name!.Foo |
! 作用在 name 上,.Foo 是普通成员访问 |
! 不是和 . 绑定的操作符——它俩独立,只是挨在一起 |
编译器会做流分析Flow Analysis,追踪变量在每条路径上的 null 状态Null State:
string? name = GetMaybeNullName();
// ❌ 编译器警告:name 可能为 null
int a = name.Length;
// ✅ 编译器不警告:你在 if 里检查过
if (name != null)
{
int b = name.Length; // name 在此分支内被追踪为 "not null"
}
// ✅ 也可以用 throw 来帮助编译器推断
if (name == null) throw new ArgumentNullException(nameof(name));
int d = name.Length; // 编译器知道这下面 name 一定不是 null
// ✅ null-coalescing 也会被追踪
int e = (name ?? "").Length;
public string Format(string input)
{
if (input == null)
throw new ArgumentNullException(
nameof(input));
return input.ToUpper();
// 你只是"自觉"保证不返回 null
}
public string Format(string input)
{
return input.ToUpper();
// 1. 非 null 签名就是契约
// 2. 调用方传 null 时编译器会警告
// 3. 你不需要手动 throw
}
ArgumentNullException.ThrowIfNull 检查(.NET 6+ 新增的)。
??= 空合并赋值Null-coalescing Assignment// C# 7.3: 检查 + 赋值 两步走
if (name == null)
name = "default";
// C# 8: 一步
name ??= "default";
// 和 ?? 一样是短路求值——只有左边是 null 才执行右边
list ??= new List<string>();
开了 NRT 后,编译器会检查所有非 null 字段是否初始化。但它在构造函数上做特殊分析——只要字段在构造函数体中被赋了值(不管是 DI 传进来还是直接 new),它就不报警告。
public class UserService
{
// 构造函数内一定被赋值 → 编译器追踪到了 → 不警告
private ILogger _logger;
private DbContext _db;
public UserService(ILogger logger, DbContext db)
{
_logger = logger;
_db = db;
}
}
#nullable enable,编译器完全不追踪 null 状态,这段代码和你 Framework 4.8 的体验完全一样。这个场景只有在 NRT 开启时才需要关心。
public class Order
{
public int Id { get; set; }
// EF Core 通过反射填充,编译器认为它没有初始化
// 用 = null! 告诉编译器"我知道它会被填充"
public Customer Customer { get; set; } = null!;
// 或标记为 required (C# 11)
// public required Customer Customer { get; set; }
}
null! 拆解——它是两个独立的东西叠加:null 给字段一个初始值(满足编译器"非 null 字段必须初始化"的要求);! 马上告诉编译器"别分析这个 null 赋值"(空包容操作符Null-forgiving Operator)。! 不是 null! 专用,它可以跟在任何表达式后面。这里的本质就是:"我先写个 null,编译器你别管,反射会在运行时塞真正的对象进来。"
// 传统 TryXxx 模式——out 参数在 NRT 下的签名
public bool TryGetUser(int id, [NotNullWhen(true)] out User? user)
{
user = _users.FirstOrDefault(u => u.Id == id);
return user != null;
}
// 调用侧——编译器理解 NotNullWhen
if (TryGetUser(42, out var user))
{
string name = user.Name; // ✅ 编译器知道这里 user 不是 null
}
[NotNullWhen(true)] 是 C# 8 引入的 null-state 分析 attribute,让编译器理解"如果方法返回 true,out 参数就不是 null"。
<Nullable>warnings</Nullable>——有提示但没编译错误#nullable enable,修完一个文件再下一个null!:对于 DI、EF Core 等"我知道它不是 null 但编译器不知道"的场景async/await 一样属于"这还用聊?"的基础设定。<Nullable>enable</Nullable>,新项目从出生就开着。string? 然后问你这行有什么问题。
#nullable enable
string? a = null;
string b = a;
string? c = "hello";
int len1 = c.Length;
int len2 = c!.Length;
string d = c!;