Tip
本文是已了解至少一种编程语言并正在学习 C# 的开发人员的 “基础知识 ”部分的一部分。 如果你不熟悉编程,请先 学习入门 教程。
来自另一种语言? C# 中的表达式的工作方式与在 Java、C++ 和 JavaScript 中的工作方式相同。 值得注意的一个区别:复合赋值运算符(如 += 和递增运算符 ++ )是 C# 中的表达式,因此它们可以显示在较大的表达式中。
表达式是由编译器求值并产生一个值的代码片段——这个值可以是数字、字符串、引用或 bool。 例如,3 + 4 * 2 是一个计算结果为整数 11 的表达式,而 total > 10 是一个计算结果为 true 或 false 的表达式。
最简单的表达式是 文本 (如 42 或 "hello")和 变量名称 (如 total)。 通过将更简单的表达式与运算符组合在一起,可以生成更复杂的表达式。
表达式和语句
表达式生成值。
语句是程序执行的完整指令。 许多语句包含表达式。 例如, int total = 3 + 4 * 2; 是变量声明语句。 编译器计算初始值设定项表达式,该表达式 3 + 4 * 2将生成 11,并将该值分配给新变量 total。
可以将表达式视为短语和 C# 语句 (如完整句子)。 这些工作定义可帮助你了解在了解有关 C# 的详细信息时会遇到的正式术语和说明,尽管普通编码很少要求有意识地记住区别。
组合表达式
表达式可以组合在一起。 以 3 + 4 * 2 为例。 此单个表达式实际上包含两个较小的表达式: 乘法表达式4 * 2 和 加法表达式3 + <result>。 当多个表达式组合在一起时,C# 需要一条规则来决定先对哪一个求值。 该规则是 运算符优先级。
无需记住完整的优先层次结构。 下一节概述了常见分组;如果你不确定,括号始终可以帮助你明确顺序。
运算符优先级
运算符优先级决定复合表达式如何分组为子表达式。 此概念类似于数学类中的运算顺序规则。 优先级较高的运算符与其操作数结合得更紧密。 该表达式的结构如同这些操作数被括在同一对括号中。 在 3 + 4 * 2 中,+ 的优先级高于 3 + (4 * 2),因此该表达式会被分组为 (3 + 4) * 2,而不是 *。
C# 的优先级组比此处汇总的四个组多。 这些其他组强制实施熟悉的规则(例如加法前乘法),并涵盖完整的运算符集。 以下组介绍了在日常代码中最常遇到的内容:
-
主要、一元和范围 - 这些是三个不同的优先组,它们都比算术更紧密地绑定。 此摘要将它们合并为一个步骤,因为出于实际目的,它们都在算术之前应用。
-
主要运算符——成员访问(
x.y)、方法调用(f())、索引(a[i])和 null 条件访问(?.、?[])——的结合最紧。 -
一元运算符作用于单个操作数:求反(
-x)、逻辑非(!flag)、前缀递增(++i)和后缀递增(i++)。 -
范围运算符 (
..) 为切片表达式生成索引范围,例如array[1..4]。
-
主要运算符——成员访问(
-
算术 -
*、/、%的绑定密切性高于+和-。 加法和减法前发生乘法和除法。 -
比较 —
<、>、>=、<=、==、!=的绑定不如算术运算紧密,因此会先完成算术运算,再进行比较。 -
逻辑运算符——
&&和||在常见运算符中绑定最松,因此会先完成比较,再进行逻辑组合。
此优先级意味着,表达式 的求值方式完全和你读它时理解的一样:将 和 相加,将和与 比较,将 与 比较,然后用 将这两个 结果组合起来。 添加括号以使每个分组显式显示相同的结构: ((score + bonus) > threshold) && (attempts < maxAttempts)
有关涵盖每个运算符的完整优先层次结构,请参阅 C# 运算符和表达式。
使用括号使意向清楚
括号会同时替代优先级并记录意向。 如果上述组中的顺序不明显,请添加括号:
int score = 80;
int bonus = 15;
int threshold = 90;
bool eligible = true;
// Without parentheses: || binds less tightly than &&, so this reads as:
// eligible && (score + bonus > threshold) || bonus > 20
bool result1 = eligible && score + bonus > threshold || bonus > 20;
// With parentheses: forces the || to combine two complete conditions
bool result2 = (eligible && score + bonus > threshold) || bonus > 20;
Console.WriteLine(result1); // => True
Console.WriteLine(result2); // => True
// Parentheses can also change the result:
bool isAdmin = false;
bool isOwner = true;
// Without: && binds tighter, so: (isAdmin && isOwner) || true
bool access1 = isAdmin && isOwner || true;
// With: forces the || to run first
bool access2 = isAdmin && (isOwner || true);
Console.WriteLine(access1); // => True (false && true is false; false || true is true)
Console.WriteLine(access2); // => False (false && anything is false)
最后两行显示括号可以更改结果,而不仅仅是样式。 当 && 和 || 一起出现时,请添加括号,以明确说明先组合哪个条件。 读者一看到 (isAdmin && isOwner) || isSuperUser 就会立刻明白其意图。
表达式的求值方式
了解 C# 如何通过类比更轻松地计算组合表达式。 考虑使用铅笔和纸张处理复杂的数学问题:你确定最内在或最优先的子表达式,计算其临时值,记下结果,然后用下一个子表达式重复-继续,直到到达最终答案。
C# 遵循相同的过程,由两个规则指导:
规则 1:操作数按从左到右的顺序求值。 对于任何二进制表达式,必须在应用运算符之前完全计算这两个操作数。 C# 先计算左操作数,再计算右操作数,然后执行该运算。
规则 2:某些运算符会短路。 某些运算符在确定结果后立即停止计算,跳过任何剩余的操作数:
-
&&(条件 AND):一旦左侧为false,则返回false。 右侧永远不会被求值。 -
||(条件 OR):一旦左侧为true,则返回true。 右侧永远不会被求值。 -
?:(条件/三元):只计算与条件匹配的分支 - 从不计算其他分支。 -
?.(null 条件成员访问运算符)和?[](null 条件元素访问):当左侧为null时,立即停止并返回null,跳过成员访问或索引。 -
??=(null 合并赋值):仅当左侧为null时,才将右侧的值赋值给左侧。
纸张和铅笔评估
考虑表达式 3 + 6 / 2。 尽管加法在阅读顺序中排在最前面,但 / 的优先级高于 +,因此 6 / 2 是最先计算的子表达式。 逐步推导 - 就像你在纸上那样:
3 + 6 / 2
↓ (evaluate 6 / 2 → 3)
3 + 3
↓ (evaluate 3 + 3 → 6)
6
// 3 + 6 / 2
// Step 1: 6 / 2 has higher precedence → interim value 3
// Step 2: 3 + 3 → final result 6
int result = 3 + 6 / 2;
Console.WriteLine(result); // => 6
由 3 生成的临时值 6 / 2 成为 + 的右操作数,最终结果为 6。 每个子表达式生成一个临时值;这些临时值将馈送下一个子表达式,直到只保留一个值。
关联性 是一个相关的概念:当两个运算符具有相同的优先级时,关联性将决定哪一个运算符优先。 大多数 C# 运算符都是 左关联运算符,这意味着它们从左到右分组。 所以a - b - c与a - (b - c)相同,不是(a - b) - c。
短路求值的实际应用
短路评估对于 null 检查特别有用:
string? text = null;
// Safe: second condition runs only when text is not null
bool hasContent = text != null && text.Length > 0;
Console.WriteLine(hasContent); // => False (short-circuits after null check; no NullReferenceException)
text = "hello";
hasContent = text != null && text.Length > 0;
Console.WriteLine(hasContent); // => True
// || short-circuits on true: right side is never evaluated when left side is true
string word = "hello";
bool anyMatch = word.StartsWith("h") || word.StartsWith("x");
Console.WriteLine(anyMatch); // => True (right side never evaluated)
// ?. short-circuits on null: returns null without accessing .Length
string? maybeNull = null;
int? length = maybeNull?.Length; // length is null; no NullReferenceException
Console.WriteLine(length.HasValue); // => False
text != null && text.Length > 0是安全的,因为第二个条件仅在null不是text时才会运行。 同样,?. 在遇到 null 引用时会停止求值,从而无需进行显式 if 检查即可避免 NullReferenceException。
有关 null 安全运算符的更广泛了解,请参阅 C# null 运算符。
另见
- C# 运算符和表达式 (语言参考) - 完全优先表和每个运算符
- 算术、比较、逻辑和赋值运算符——常见运算符详解
-
相等比较 —
!=、==和Equals的工作原理 -
C# null 运算符 —
?.、??和??= - Boolean 逻辑运算符