C# null 运算符

小窍门

本文是了解至少一种编程语言并正在学习 C# 的开发人员的 “基础知识 ”部分的一部分。 如果你不熟悉编程,请先 学习入门 教程。 有关完整的运算符参考,请参阅语言参考中的 成员访问运算符空合并运算符

C# 提供了多个运算符,使 null 安全代码简洁。 这些运算符允许在单个表达式中表达 null 安全访问、回退值和 null 测试,而不是在整个代码中嵌套 if (x != null) 保护。

本文介绍?.?[]用于 null 条件访问、 ?? null 合并、 ??= null 合并分配以及is null/is not nullnull 模式匹配。

Null 条件成员访问 ?.

仅当对象不为空时,?. 运算符才访问其成员。 当对象是null时,整个表达式的计算结果为null,而不是引发NullReferenceException

string? name = null;

// Without ?., accessing a member on null throws NullReferenceException:
// int len = name.Length; // throws if name is null

// ?. returns null instead of throwing:
int? len = name?.Length;
Console.WriteLine(len.HasValue); // False

name = "C#";
Console.WriteLine(name?.Length); // 2

?. 运算符短路:当左侧是 null 时,右侧的所有内容都会被跳过。 不会运行任何方法调用,也不会产生副作用。

可以在单个表达式中链接多个 ?. 运算符。 链在遇到的第一个 null 处停止:

string? input = null;

// Chain ?. across multiple method calls — short-circuits at the first null:
string? upper = input?.Trim()?.ToUpperInvariant();
Console.WriteLine(upper ?? "(none)"); // (none)

input = "  hello  ";
Console.WriteLine(input?.Trim()?.ToUpperInvariant()); // HELLO

null 条件索引器访问 ?[]

运算符 ?[] 对索引器和数组访问应用相同的短路行为。 当集合本身可能为 null

string[]? tags = null;

// ?[] accesses an element only when the collection is non-null
string? first = tags?[0];
Console.WriteLine(first ?? "(none)"); // (none)

tags = ["csharp", "dotnet", "nullable"];
Console.WriteLine(tags?[0]);          // csharp

链 null 条件运算符

链接多个 ?. 运算符以遍历可能为 null 引用的路径。 链在第一个null处短路:

var order = new Order("ORD-001", null);

// Each ?. short-circuits when null: Customer is null, so Address and City are never accessed
string? city = order.Customer?.Address?.City;
Console.WriteLine(city ?? "(no city)"); // (no city)

var fullOrder = new Order("ORD-002",
    new Customer("Alice", new Address("123 Main St", "Springfield", "IL")));

Console.WriteLine(fullOrder.Customer?.Address?.City); // Springfield

Customernull时,既不评估Address,也不评估City。 整个表达式返回 null

线程安全的委托调用

?. 提供一种干净、线程安全的方法来调用委托或引发事件。 委托表达式只计算一次,因此在进行空值检查和调用之间,没有机会让其他线程取消订阅。

EventHandler? clicked = null;

// No subscribers — ?.Invoke does nothing instead of throwing NullReferenceException
clicked?.Invoke(null, EventArgs.Empty);

clicked += (_, _) => Console.WriteLine("Button clicked!");

// With a subscriber — ?.Invoke calls the handler
clicked?.Invoke(null, EventArgs.Empty);
// Output: Button clicked!

此模式取代了较旧的 if (clicked != null) clicked(...) 成语。

null 合并 ??

?? 操作数为非 null 时,运算符返回其左侧操作数,当左侧操作数为 null 时返回其右侧操作数。 使用它提供默认值:

string? username = null;

// ?? returns the right-hand value when the left-hand is null
string display = username ?? "Guest";
Console.WriteLine(display); // Guest

username = "alice";
display  = username ?? "Guest";
Console.WriteLine(display); // alice

?? 是右关联,因此 a ?? b ?? c 计算结果为 a ?? (b ?? c). 第一个非 null 值获胜。 常见的模式是将?.??进行链式调用:使用?.安全地遍历可能为null的链,然后如果链返回??,使用null替换默认值。 有关完整示例,请参阅 “合并 null 运算符”。

Null 合并赋值 ??=

仅当变量为??=变量时,运算符null才将右侧值赋给变量。 将其用于延迟初始化:

List<string>? cache = null;

// ??= assigns only when the variable is null
cache ??= LoadData();
Console.WriteLine(cache.Count); // 3

// cache is already non-null, so LoadData() isn't called again
cache ??= LoadData();
Console.WriteLine(cache.Count); // 3

static List<string> LoadData() => ["alpha", "beta", "gamma"];

仅当变量为 null 时,才计算右侧表达式。 当变量已有一个值时,右侧将不会被计算。

Null 条件赋值 (C# 14)

从 C# 14 开始,可以使用 ?.?[] 分配目标。 只有当左侧对象为非空时,赋值才会执行。

AppConfig? config = new AppConfig();

// Assigns only when config is non-null (C# 14)
config?.Theme = "dark";
Console.WriteLine(config?.Theme); // dark

AppConfig? missing = null;
missing?.Theme = "light";                         // no-op: missing is null
Console.WriteLine(missing?.Theme ?? "(no config)"); // (no config)

仅当已知左侧为非 Null 时,才会计算右侧。

Null 模式匹配: is nullis not null

is nullis not null模式测试表达式是否为null

string? input = null;

// is null is the preferred test — unaffected by operator overloading
if (input is null)
{
    Console.WriteLine("No input provided.");
}

// == null also works, but a custom == operator can change its behavior
if (input == null)
{
    Console.WriteLine("Still no input.");
}

首选使用 is null 而非 == null 来进行空值检查。 如果类型定义了自定义相等运算符,则==运算符可以重载,这意味着即使x == null不是truex也可能返回null。 无论运算符重载如何,模式 is null 始终都会检测实际的 null 引用。

string? value = "hello";

if (value is not null)
{
    Console.WriteLine(value.ToUpper()); // HELLO
}

合并 空值运算符

实际上,通常会组合其中几个运算符。 一个表达式可以安全地遍历深度对象图、应用回退,然后保护结果:

Order? order = GetPendingOrder();

// Chain ?. for safe traversal, ?? for a fallback, is null for a clear guard
string city = order?.Customer?.Address?.City ?? "unknown";

if (order is null)
{
    Console.WriteLine("No pending order.");
}
else
{
    Console.WriteLine($"Shipping to: {city}");
}
// Output: No pending order.

null 包容运算符 !

! 后缀运算符禁止显示可为 null 的警告。 追加 ! 以告知编译器“此表达式绝对不为 null”。运算符在运行时不起作用。 它只影响编译器的 null 状态分析。

string? name = FindUser("alice");

// Use ! only when you have information the compiler doesn't.
// FindUser guarantees a non-null result for known usernames.
int length = name!.Length;
Console.WriteLine(length); // 5

请谨慎使用 ! ,并且仅当您拥有编译器所没有的信息时才使用。 示例包括有意传递 null 以验证参数检查逻辑的测试,或调用一个协定保证已知输入会返回非空值的方法。 过度使用 ! 会破坏可为 null 引用类型的用途。 有关完整说明,请参阅 可空引用类型

另见