?? 是空合并运算符:用默认值兜底 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,就不会再进
List<string> items = null;
items ??= new List<string>(); // 第一次:赋值
items.Add("first");
<p>int? value = null;
value ??= 42; // value 现在是 42
value ??= 99; // 不执行,value 仍是 42?? 和 ??= 的关键区别在哪
核心不在“有没有等号”,而在于「是否修改左操作数」和「适用上下文」:
??是纯表达式,返回一个值,不改变任何变量(类似
a + b)
??=是赋值语句,必须左操作数可写(
var声明的局部变量、字段、
ref参数、属性 set 访问器内等) 两者都支持泛型约束:可与
T?或引用类型泛型参数一起用,但不能用于不可空的
struct(除非显式转为可空) 结合使用很自然:
cache ??= new Dictionary<string string>(); cache["key"] = value ?? "default";</string>
替代方案对比:为什么推荐 ?? / ??= 而不是 if 或 ?:?
它们不是功能上不可替代,而是更安全、更简洁、更符合现代 C# 的空安全设计意图:
if (x == null) x = y;多行、易漏锁、无法嵌入表达式中(比如 LINQ 查询里)
x != null ? x : y冗余判断(
x被算两次)、对引用类型不安全(竞态下可能中途变 null)
GetValueOrDefault()仅适用于可空值类型,且不能指定任意默认值(只能用默认构造)
??和
??=编译后生成高效 IL,没有运行时反射或装箱开销,且被 Roslyn 和 IDE 全面支持(智能提示、null 分析警告)
真正容易被忽略的是:这两个运算符和
?.(空条件调用)配合使用时,能构建出极简又健壮的空安全链式表达式,比如
config?.Features?.TimeoutMs ?? 30000—— 这种写法一旦习惯,就很难退回手写 null 检查了。
