赋值与复合赋值、空合并
本教程共 100 篇 · 第 16 篇 · 更新于 2026-07-31 · 约 8 分钟阅读
16. 赋值与复合赋值、空合并
本节目标:学完能熟练用复合赋值化简累加逻辑,并用
??和??=优雅地处理可能为空的值。
赋值是最基础的运算,但 C# 在它之上加了几样非常实用的糖:+= 这类复合赋值,以及专门对付「空值」的空合并运算符。这一章把它们一次讲清。
一、简单赋值:把右边交给左边
= 是赋值运算符,作用是把右边的值存进左边的变量。注意它不是「等于」,等于是上一章的 ==。
int count = 10; // 把 10 交给 count
count = 20; // 重新赋值,count 现在是 20
赋值是「右结合」的,所以可以写成连等:
int x, y, z;
x = y = z = 5; // 从右往左:z=5,y=5,x=5
Console.WriteLine($"{x} {y} {z}");
Note新手最容易把
if (x == 5)写成if (x = 5)。后者在 C# 里通常是编译错误(因为int不能当bool),编译器帮你挡住了这个经典 bug。这比 C 语言友好得多。
二、复合赋值:算完再存回去
当你要「基于自身做运算再赋值」时,复合赋值让代码更短。最常见的形式是 +=。
int sum = 0;
sum += 5; // 等价于 sum = sum + 5
sum *= 2; // 等价于 sum = sum * 2
Console.WriteLine(sum); // 输出 10
完整的复合赋值家族包括:
| 运算符 | 等价于 |
|---|---|
+= | x = x + y |
-= | x = x - y |
*= | x = x * y |
/= | x = x / y |
%= | x = x % y |
&= ` | = ^=` |
<<= >>= | 移位后赋值 |
它不只省字数,还避免了重复写变量名。下面这个拼接场景尤其常见:
string log = "";
log += "开始\n";
log += "处理中\n";
log += "结束\n";
Console.WriteLine(log);
Tip上例用
+=拼少量字符串没问题;若要在循环里大量拼接,记得第 14 章说的,换成StringBuilder才高效。
三、空合并 ??:给空值一个备胎
?? 叫空合并运算符(null-coalescing)。它会返回「左边若不为空就用左边,否则用右边」。
string? name = null; // 可能为 null
string display = name ?? "匿名";
Console.WriteLine(display); // 输出 匿名
它的妙处在于:只在左边为 null 时才去算右边,右边的计算具有短路性。比起写 if (name == null) name = "匿名";,一行就搞定,干净利落。
链式兜底
多个 ?? 可以连起来,从左到右取第一个非空值:
string? a = null;
string? b = null;
string c = "默认值";
string result = a ?? b ?? c; // 取第一个非空,即 "默认值"
Console.WriteLine(result);
四、空合并赋值 ??=:空了才赋值
??=(C# 8 引入)是 ?? 的赋值版:只有当左边为 null 时,才把右边赋给它。它把「为空则初始化」这个常见动作压成一行。
string? config = null;
config ??= "默认配置"; // config 是 null,所以赋值为 "默认配置"
Console.WriteLine(config);
config ??= "另一配置"; // config 已非空,这次赋值不发生
Console.WriteLine(config); // 仍是 默认配置
它和 ?? 的区别:?? 只「取值」不修改变量;??= 会在条件满足时「写回」变量。下面这个缓存初始化的例子最能说明价值:
List<string>? cache = null;
cache ??= []; // 第一次访问时若还没建,就新建一个空列表
cache.Add("数据");
Console.WriteLine(cache.Count);
Note可空性标记里的
?(如string?)只是给编译器和分析工具看的提示,表示「这个值可能为 null」。真正做运行期兜底,靠的是本章的??和??=。
五、在一起用也顺手
这三种运算符可以组合,写出既安全又紧凑的代码:
string? input = Console.ReadLine();
string value = input?.Trim() ?? "(空)"; // ?. 为空就短路,?? 给默认值
Console.WriteLine(value);
Tip本节三个工具的心法:
=负责存,+=负责「累加后再存」,??/??=负责「为空时给个保底」。它们单独都简单,合起来能消除大量啰嗦的if判空代码。
六、空合并也适用于值类型(可空值类型)
前面例子里 ?? 多用来兜底字符串,但它对值类型同样好使。C# 里 int?、double? 这种带问号的类型叫「可空值类型(nullable value type)」,本质是 Nullable<T> 的语法糖,它允许一个本不可为空的值(如 int)暂时表示「没有值」。遇到这种「可能没填」的数字,用 ?? 给个保底值最自然:
int? age = null; // 年龄没填
int realAge = age ?? 0; // 没填就按 0 算
Console.WriteLine(realAge); // 输出 0
int? score = 85;
int actual = score ?? -1; // 有值就用 85
Console.WriteLine(actual); // 输出 85
??= 也能用在可空值类型上,做「没设就补默认」的就地初始化:
int? retry = null;
retry ??= 3; // 没设重试次数,默认 3 次
Console.WriteLine(retry); // 输出 3
Tip可空值类型常用于「可选的数字配置」:比如用户没填年龄、没设重试次数。比起用
-1之类的「魔法数字」占位,用null表达「没填」、再用??兜底,语义更清楚,也少踩坑。
七、运算符优先级:空合并的小陷阱
?? 和 ??= 的优先级(precedence)比多数运算符都低。新手常踩的一个坑是:把 ?? 和三元运算符 ?: 直接连写时,必须给三元部分加括号,否则编译器会报错或结果不符合预期。
string? name = null;
int score = 85;
// 三元部分务必套括号,意图一眼就清
string label = name ?? (score >= 60 ? "及格" : "不及格");
Console.WriteLine(label);
另外注意:?? 的左边必须是「可以为 null」的类型。把 ?? 用在永远不为 null 的变量上毫无意义,编译器也会提醒你。
Warning不要写
a ?? b ? c : d这种不加括号的混合表达式。不同 C# 版本对它的宽容度不同,读代码的人也极易误判求值顺序。任何「空合并 + 三元」的组合,都给三元套上()。
八、为什么语言要加这些糖:设计动机
你可能会想:?? 不就是 x == null ? 默认值 : x 的简写吗?确实等价,但语言设计者专门造一个运算符,理由有三:
- 意图更聚焦。
name ?? "匿名"读起来就是「name 为空就用匿名」,而三元写法要先写判断再写两个分支,重点被稀释。 - 只算一次右边。
??是「短路」的:左边非空时,右边根本不会求值。如果右边是个昂贵的方法调用,??能省掉这次开销,手写if稍不留神就会多算一次。 - 配合
??=表达「懒初始化」。把「没值才填」压成一行,是if (x == null) x = ...无法在一行内优雅表达的。
一句话:语法糖不是花架子,它让「常见且正确的写法」变得最短、最不容易写错。
九、常见组合招式
把 ?.(空条件)、??(空合并)和 ??= 串起来,能写出既安全又极短的代码:
string? input = Console.ReadLine();
string trimmed = input?.Trim() ?? "(空或没输入)";
Console.WriteLine(trimmed);
List<string>? history = null;
history ??= []; // 第一次用就建好
history.Add("打开页面");
Console.WriteLine(history.Count);
Note
?.和??是黄金搭档:?.负责「前面为空就别往下点」,一旦短路返回null,??立刻接上给默认值。两者合起来,把一整段判空if压成一行。
?? 和 ?: 怎么选
两者都能给默认值,但角度不同:
??专管”空”:左边为null才用右边。语义聚焦,读起来就是”为空则兜底”。?:三元运算符是按任意bool二选一:条件 ? A : B,不限于空判断。
如果只是”为空用默认”,优先 ??,意图最清楚:
string name = input ?? "匿名"; // 专注空兜底
string label = (input != null) ? input : "匿名"; // 等价但啰嗦
Tip遇到”可能为空就给默认值”的诉求,先想
??/??=,别本能地写if (x == null) x = ...。前者一行到位,后者多三行。
复合赋值还有哪些
除了 +=,全套算术都有对应复合版:+=、-=、*=、/=、%=。位运算也有:&=、|=、^=、<<=、>>=。它们的逻辑一致:左边和右边运算后存回左边。平时最常用 +=,其余按场景出现。
Note复合赋值有个隐蔽点:它只求值一次左边。若左边是复杂表达式(如
array[i++] += 1),i++只发生一次。正常代码里左边用简单变量即可避开这种纠结。
常见疑问解答
问:??= 和 ?? 到底差在哪?
?? 是”取一个值”(结果赋给别的变量);??= 是”必要时才改自己”。若你只是要一个兜底后的值去显示,用 ??;若要”没值就填上、有值就不动”这种就地初始化,用 ??=。
问:复合赋值会改变类型吗?
不会。x += 1 的类型仍是 x 的类型。但注意 x += 1.5 当 x 是 int 时会编译报错(不能把 double 赋给 int),和 x = x + 1.5 一样严格。
问:空合并能连写多个吗?
能。a ?? b ?? c 表示”依次找第一个不为 null 的”。常用于多级兜底默认值,比嵌套 if 清爽。但别连太多,三四个封顶,再多就该重构了。
要点速记
=是赋值;+=系列是”运算后存回”。??取左边,为空取右边(兜底取值)。??=仅当左为空才赋值(C# 8 引入)。- 处理可空值,优先
??/??=而非啰嗦if。
动手练一练
- 写
int sum = 0;用sum += 5累加三次后打印,体会复合赋值。 - 定义
string? name = null;,用??给它兜底成"匿名"并打印。 - 用
??=给一个可能为空的配置变量赋值默认值,验证”有值不动、无值才填”。
??= 与懒加载
??= 最经典的用武之地是”懒加载”(lazy loading):资源第一次要时才创建,之后复用。
List<string> _cache;
List<string> GetCache()
{
_cache ??= new List<string> { "初始化数据" };
return _cache;
}
第一次调用 _cache 为空,于是建好;之后调用发现已有,直接复用。一行 ??= 替代了四五行的 if (_cache == null) 判空,干净又不易错。
复合赋值的返回值能用吗
注意:x += 1 在 C# 里既是语句、也是表达式,整体会返回”赋值之后的新值”。所以在 C# 里 int y = (x += 1); 是合法写法:它先把 x 加 1,再把新值赋给 y,能正常编译。不过这种”一行顺带两个赋值”的连写可读性差,容易让人看漏,所以不推荐这么写,需要时分开写两行更清晰。
小结与下一步
= 把值存进变量,+= 让”算完再存”变一行,?? 和 ??= 把”为空就兜底”写得既安全又紧凑。它们单独简单,合起来能消掉大量啰嗦的判空代码。
下一章进入控制流,先讲条件判断 if/else 和 switch 的基础用法。