痛点: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

→ 下一篇 异常处理