?? 返回左侧值(非null)或右侧默认值,??= 仅在左侧为null时赋值;二者均懒求值、不抛异常,且需左操作数为可空类型或引用类型。

?? 是空合并运算符:用默认值兜底 null
当左侧表达式为 null(或可空类型的 null)时,?? 直接返回右侧值;否则就原样返回左侧值。它不抛异常、不执行右侧表达式(懒求值),适合快速设默认值。
- 常见错误:写成
a = b ?? "default"却没意识到b是非可空类型(如int),编译直接报错——??要求左操作数必须是可空类型(T?)或引用类型 - 典型场景:配置读取、API 返回值处理、UI 字段 fallback
- 性能注意:右侧表达式只在左侧为
null时才执行,比如GetDefaultName() ?? "Guest"中,GetDefaultName()不会无故调用
string name = user?.Name ?? "Anonymous"; int? count = data?.Length ?? 0; // data 可能为 null,Length 是 int?,所以合法 // ❌ 错误示例(编译失败): // int i = 5 ?? 10; // int 不可为 null,不能用 ??
??= 是空合并赋值运算符:只在 null 时才赋值
??= 是 C# 8.0+ 引入的语法糖,作用等价于 “如果左边是 null,就把右边值赋给它”,且同样跳过右侧计算(懒赋值)。它不是简单缩写 a = a ?? b,而是语义更精确的“仅当需要时才初始化”。
- 必须满足:左操作数是变量、属性或索引器访问(即有“存储位置”),不能是常量或方法调用结果
- 典型场景:延迟初始化缓存、集合首次填充、避免重复 new 对象
- 容易踩坑:连续链式使用时,右侧表达式可能被多次求值(如果没封装好),例如
cache ??= GetNewCache()每次都调用GetNewCache()—— 但只要cache非 null,就不会再进
Listitems = null; items ??= new List (); // 第一次:赋值 items.Add("first"); int? value = null; value ??= 42; // value 现在是 42 value ??= 99; // 不执行,value 仍是 42
?? 和 ??= 的关键区别在哪
核心不在“有没有等号”,而在于「是否修改左操作数」和「适用上下文」:
-
??是纯表达式,返回一个值,不改变任何变量(类似a + b) -
??=是赋值语句,必须左操作数可写(var声明的局部变量、字段、ref参数、属性 set 访问器内等) - 两者都支持泛型约束:可与
T?或引用类型泛型参数一起用,但不能用于不可空的struct(除非显式转为可空) - 结合使用很自然:
cache ??= new Dictionary(); cache["key"] = value ?? "default";
替代方案对比:为什么推荐 ?? / ??= 而不是 if 或 ?:?
它们不是功能上不可替代,而是更安全、更简洁、更符合现代 C# 的空安全设计意图:
-
if (x == null) x = y;多行、易漏锁、无法嵌入表达式中(比如 LINQ 查询里) -
x != null ? x : y冗余判断(x被算两次)、对引用类型不安全(竞态下可能中途变 null) -
GetValueOrDefault()仅适用于可空值类型,且不能指定任意默认值(只能用默认构造) -
??和??=编译后生成高效 IL,没有运行时反射或装箱开销,且被 Roslyn 和 IDE 全面支持(智能提示、null 分析警告)
真正容易被忽略的是:这两个运算符和 ?. (空条件调用)配合使用时,能构建出极简又健壮的空安全链式表达式,比如 config?.Features?.TimeoutMs ?? 30000 —— 这种写法一旦习惯,就很难退回手写 null 检查了。









