?? 和 ??= 是具有短路语义的运算符,非语法糖;?? 确保右侧仅在左侧为 null 时求值,??= 要求左侧为可写左值,二者均依赖静态类型推断,泛型或反射场景下失效。

?? 和 ??= 不是语法糖,而是有明确语义和执行时机的短路运算符。用错地方会引发意外计算、空引用或延迟初始化失效。
什么时候该用 ?? 而不是 == null ? x : y
核心区别在于右侧表达式是否会被求值。如果右侧是带副作用的操作(比如构造新对象、调用方法、访问数据库),?? 能确保它只在左侧为 null 时才执行。
-
??左侧为非null时,右侧完全不执行 —— 这是编译器保证的短路行为 -
== null ? x : y中,x和y都会在三元运算前被求值(除非是 lambda 或委托包装) - 常见误用:把
new List<int>()直接写在三元右侧,导致每次判断都新建实例 - 正确写法:
list ?? new List<int>()或更推荐的list ??= new List<int>()(见下节)
??= 的赋值目标必须是可写左值
??= 看似方便,但它的左侧不能是任意表达式 —— 必须能被赋值,也就是变量、属性、索引器元素。否则编译直接报错。
- ✅ 合法:
numbers ??= new List<int>()(numbers是变量) - ✅ 合法:
config.Settings ??= new Dictionary<string, string>()(Settings是可写属性) - ❌ 非法:
GetList() ??= new List<int>()(方法调用结果不可赋值) - ❌ 非法:
items[0] ??= 42(若items是只读集合或item[0]是 getter-only 索引器) - ⚠️ 注意:字段/属性若为
readonly或init,即使类型可空,??=也会编译失败
链式 ?? 的优先级和结合性容易被忽略
?? 是右结合的,这意味着 a ?? b ?? c 等价于 a ?? (b ?? c),而不是 (a ?? b) ?? c。这在嵌套配置或 fallback 场景中直接影响结果。
- 典型场景:从多个层级取配置值,希望按顺序 fallback:
user.Config?.Theme ?? app.DefaultTheme ?? "light" - 错误写法:
user.Config?.Theme ?? app.DefaultTheme ?? "light"写成(user.Config?.Theme ?? app.DefaultTheme) ?? "light"—— 实际效果一样,但显式括号反而掩盖了右结合本质 - 陷阱示例:
string s = null; var x = s ?? throw new InvalidOperationException() ?? "fallback"—— 编译失败,因为throw表达式不是值类型,不能作为??右操作数 - 性能提示:连续多个
??不会重复计算左侧,但每个??的右操作数仍需独立求值(除非被前面的非null值短路)
?? 和 ?. 组合使用时的空传播顺序
很多人以为 obj?.Prop ?? "default" 是“先判空再合并”,其实它严格按运算符优先级拆解:(obj?.Prop) ?? "default"。也就是说,?. 先执行,返回 null 或实际值,再由 ?? 处理这个结果。
- ✅ 安全:
person?.Name ?? "未知"—— 若person为null,?.返回null,??捕获并替换 - ✅ 安全:
config?.Values?[0] ?? -1—— 空条件索引返回null(当Values为null或索引越界),再由??提供默认 - ⚠️ 注意:
??不会阻止?.抛异常 —— 如果person不为null但person.Name是个抛异常的 property,??无能为力 - ⚠️ 注意:
?.返回类型必须和??右侧类型兼容,比如string?和string可以,但int?和string就会编译失败
真正容易被绕过的点是:所有这些运算符都依赖类型系统对可空性的静态推断。一旦涉及泛型、动态类型或反射,?? 和 ??= 就会失效或行为出人意料 —— 这时候只能退回到显式的 if 或 is null 判断。


















