痛点:NullReferenceException
string s = null;
s.Length; // 💥 NullReferenceException
10 亿美元错误(Tony Hoare 自嘲发明 null)——C# 8 之前防不胜防。
启用可空引用类型
.csproj:
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
或单文件:#nullable enable 顶部。新项目模板默认开启。
启用后:
string s = null; // ⚠️ 编译警告:null 不能赋给非空 string
string? s = null; // ✅ string? 是可空版本
注意:这只是编译时检查——运行时 string? 还是和 string 一样,没真正强制。
标注
string name; // 非空(不会是 null)
string? maybe; // 可空
int n; // 值类型默认就允许 0
int? maybe2; // Nullable<int>,能为 null
string? 的 ? 是给编译器的标注——它不改变运行时类型,只让编译器知道"这可能是 null"。
检查后使用
string? input = Console.ReadLine();
// ❌
Console.WriteLine(input.Length); // 警告:input 可能是 null
// ✅ 检查
if (input is not null)
{
Console.WriteLine(input.Length); // 编译器知道这里 input 不为 null
}
// ✅ Null 条件运算符 ?.
Console.WriteLine(input?.Length); // input 为 null 时整个表达式为 null
// ✅ Null 合并 ??
var len = input?.Length ?? 0;
空合并赋值 ??=
string? name = null;
name ??= "Anonymous"; // name 为 null 时才赋值
! :null-forgiving 运算符
string? maybe = GetMaybe();
if (HasValue()) // 你知道一定有,但编译器不知道
{
Console.WriteLine(maybe!.Length); // ! 告诉编译器"信我,不为 null"
}
慎用——! 是给编译器的承诺,承诺错了一样会 NRE。最好通过模式 / 检查让编译器自己推断。
警告级别
<Nullable>enable</Nullable> <!-- 完整:警告 + 标注 -->
<Nullable>annotations</Nullable> <!-- 只允许标注,不报警告 -->
<Nullable>warnings</Nullable> <!-- 报警告,不允许标注 -->
<Nullable>disable</Nullable> <!-- 全关 -->
老项目迁移:annotations 先标 → 修警告 → 切 enable。
方法签名
public User? FindUser(int id) // 可能返回 null
{
return db.Users.FirstOrDefault(u => u.Id == id);
}
public User GetUser(int id) // 保证不为 null
{
return db.Users.First(u => u.Id == id); // 找不到抛异常
}
public bool TryGetUser(int id, [NotNullWhen(true)] out User? user)
{
user = db.Users.FirstOrDefault(u => u.Id == id);
return user is not null;
}
[NotNullWhen(true)] 等属性告诉编译器更复杂的关系——"当返回 true 时,out 参数不为 null"。
字段 / 属性
public class Person
{
public string Name { get; set; } // ⚠️ 警告:非空属性必须初始化
public string Name2 { get; set; } = ""; // ✅
public string? Email { get; set; } // ✅
public required string Email3 { get; set; } // ✅ C# 11+
public string FullName; // 字段同理
private string _backing = ""; // 显式初始化
}
构造函数
public class Service
{
private readonly string _connStr;
public Service(string connStr)
{
_connStr = connStr ?? throw new ArgumentNullException(nameof(connStr));
}
}
构造里赋值不为 null 的非空字段——编译器满意。
泛型 T?
public class Stack<T>
{
private T[] _items = new T[10];
private int _top = 0;
public void Push(T item) => _items[_top++] = item;
// T? 在引用类型 / 值类型表现不同
public T? Pop()
{
if (_top == 0) return default;
return _items[--_top];
}
}
T? 的含义:
- T 是引用类型(class)→ T? 是可空引用类型
- T 是值类型(struct)→ T? 是 Nullable
C# 9 起处理统一。
不可空 → 可空:直接赋值
string s = "hi";
string? maybe = s; // 隐式(窄类型给宽类型)
反过来不行(编译警告):
string? maybe = ...;
string s = maybe; // ⚠️ 警告
库的 API 设计
公开 API 务必精确标注:
// ❌ 含糊
public string GetConfig(string key) { ... } // 找不到时返回 null?还是抛?
// ✅ 显式
public string? GetConfig(string key) { ... } // 找不到时返 null
public string GetConfigOrThrow(string key) { ... } // 找不到时抛
帮使用者写出对的代码。
实战清单
- 新项目
<Nullable>enable</Nullable>默认开 - API 返回值 / 参数显式标注
? - 不滥用
!——它能消警告但不消问题 - 模式匹配
is not null替代!= null - 字段 / 属性初始化或标
required
→ 下一篇 异常处理