首页 / C# 入门教程 / 赋值与复合赋值、空合并

C# 入门教程

赋值与复合赋值、空合并

本教程共 100 篇 · 第 16 篇 · 更新于 2026-07-31 · 约 8 分钟阅读

C#C# 入门教程编程语言赋值空合并????=

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 的简写吗?确实等价,但语言设计者专门造一个运算符,理由有三:

  1. 意图更聚焦name ?? "匿名" 读起来就是「name 为空就用匿名」,而三元写法要先写判断再写两个分支,重点被稀释。
  2. 只算一次右边?? 是「短路」的:左边非空时,右边根本不会求值。如果右边是个昂贵的方法调用,?? 能省掉这次开销,手写 if 稍不留神就会多算一次。
  3. 配合 ??= 表达「懒初始化」。把「没值才填」压成一行,是 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.5xint 时会编译报错(不能把 double 赋给 int),和 x = x + 1.5 一样严格。

问:空合并能连写多个吗? 能。a ?? b ?? c 表示”依次找第一个不为 null 的”。常用于多级兜底默认值,比嵌套 if 清爽。但别连太多,三四个封顶,再多就该重构了。

要点速记

  • = 是赋值;+= 系列是”运算后存回”。
  • ?? 取左边,为空取右边(兜底取值)。
  • ??= 仅当左为空才赋值(C# 8 引入)。
  • 处理可空值,优先 ??/??= 而非啰嗦 if

动手练一练

  1. int sum = 0;sum += 5 累加三次后打印,体会复合赋值。
  2. 定义 string? name = null;,用 ?? 给它兜底成 "匿名" 并打印。
  3. ??= 给一个可能为空的配置变量赋值默认值,验证”有值不动、无值才填”。

??= 与懒加载

??= 最经典的用武之地是”懒加载”(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/elseswitch 的基础用法。