C# 表达式

Tip

本文是已了解至少一种编程语言并正在学习 C# 的开发人员的 “基础知识 ”部分的一部分。 如果你不熟悉编程,请先 学习入门 教程。

来自另一种语言? C# 中的表达式的工作方式与在 Java、C++ 和 JavaScript 中的工作方式相同。 值得注意的一个区别:复合赋值运算符(如 += 和递增运算符 ++ )是 C# 中的表达式,因此它们可以显示在较大的表达式中。

表达式是由编译器求值并产生一个值的代码片段——这个值可以是数字、字符串、引用或 bool。 例如,3 + 4 * 2 是一个计算结果为整数 11 的表达式,而 total > 10 是一个计算结果为 truefalse 的表达式。

最简单的表达式是 文本 (如 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# 的优先级组比此处汇总的四个组多。 这些其他组强制实施熟悉的规则(例如加法前乘法),并涵盖完整的运算符集。 以下组介绍了在日常代码中最常遇到的内容:

  1. 主要、一元和范围 - 这些是三个不同的优先组,它们都比算术更紧密地绑定。 此摘要将它们合并为一个步骤,因为出于实际目的,它们都在算术之前应用。
    • 主要运算符——成员访问(x.y)、方法调用(f())、索引(a[i])和 null 条件访问(?.?[])——的结合最紧。
    • 一元运算符作用于单个操作数:求反(-x)、逻辑非(!flag)、前缀递增(++i)和后缀递增(i++)。
    • 范围运算符 (..) 为切片表达式生成索引范围,例如array[1..4]
  2. 算术 - */% 的绑定密切性高于 +-。 加法和减法前发生乘法和除法。
  3. 比较<>>=<===!= 的绑定不如算术运算紧密,因此会先完成算术运算,再进行比较。
  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 - ca - (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 运算符

另见