Skip to content

C# 运算符优先级:从数学课代表到代码裁判官 ​

开篇:为什么你的代码会「算错账」? ​

想象你是一家餐厅的收银员。顾客点了一份 10 元的炒饭和一份 5 元的汤,使用「满 20 减 5」的优惠券。你该怎么算?

csharp
// 场景:计算最终价格
int rice = 10, soup = 5;
int discount = 5;

// ❌ 错误写法:先加后减?还是先减后加?
int total = rice + soup - discount;  // 10 + 5 - 5 = 10(正确)

// 但如果表达式更复杂呢?
int total2 = rice + soup * 2 - discount;  // 你的预期是多少?
// 实际执行:rice + (soup * 2) - discount = 10 + 10 - 5 = 15
// 如果你预期:(rice + soup) * 2 - discount = 25,那就错了!

这就是运算符优先级的核心价值:它是代码的「裁判官」,决定当多个运算符混在一起时,谁先算、谁后算。不懂优先级,你的代码就会像没有裁判的球赛——看似在跑,实则乱套。


一、优先级总览:C# 的「运算江湖座次表」 ​

C# 官方规范定义了 18 个优先级等级,从高到低排列。高优先级的运算符先执行,同级的按结合性(左结合或右结合)依次执行。

1.1 优先级速查总表 ​

优先级类别运算符结合性
1Primary 基础x.y、f(x)、a[i]、x?.y、x++、x--、new、typeof、sizeof左
2一元+、-、!、~、++x、--x、(T)x、&x右
3乘除模*、/、%左
4加减+、-左
5移位<<、>>左
6关系比较<、>、<=、>=、is、as左
7相等比较==、!=左
8位与&左
9位异或^左
10位或|左
11逻辑与&&左
12逻辑或||左
13Null 合并??右
14条件(三元)?:右
15赋值/Lambda=、+=、-=、*=、=>右

记忆锚点:?? 和 ?: 是右结合,其余二元运算符几乎都是左结合。


二、逐层拆解:从「江湖老大」到「垫底小弟」 ​

2.1 Primary 基础运算符:优先级之王(第 1 级) ​

这些是「最先行动」的运算符,像 VIP 客户一样优先处理。

csharp
// 成员访问 .
int length = "hello".Length;  // . 优先,先取 Length 属性

// 方法调用 ()
int result = Math.Max(1, 2);  // () 优先,先执行方法

// 数组索引 []
int[] arr = { 10, 20, 30 };
int first = arr[0];           // [] 优先,先取元素

// 空条件成员 ?.(C# 6.0+)
string name = user?.Name;     // ?. 优先,先判断 user 是否为 null

// 后缀自增/自减
int i = 5;
int a = i++;  // i++ 优先级最高,但后缀模式:先取 i 的值 5,再自增
// a = 5, i = 6

易错点:x++(后缀)和 ++x(前缀)虽然都是第 1-2 级,但求值时机不同。

csharp
int i = 5;
int a = i++;  // 后缀:先取值 5,再自增 → a=5, i=6
int b = ++i;  // 前缀:先自增,再取值 7 → b=7, i=7

2.2 一元运算符:单操作数的「独行侠」(第 2 级) ​

一元运算符只作用于一个操作数,右结合(从右到左)。

csharp
int x = 5;

// 正号 +(通常省略)
int positive = +x;    // +5

// 负号 -
int negative = -x;    // -5

// 逻辑非 !
bool flag = !true;    // false

// 位非 ~(按位取反)
int bits = ~5;        // ~00000101 = 11111010 = -6(补码)

// 前缀自增/自减
int a = ++x;  // x 先变 6,a = 6
int b = --x;  // x 先变 5,b = 5

// 强制类型转换 (T)
double d = 3.14;
int n = (int)d;       // 截断为 3

// 取地址 &(unsafe 代码)
// unsafe { int* ptr = &x; }

右结合演示:

csharp
// !!x 等价于 !(!x),从右到左
bool result = !!true;  // 先算 !true = false,再算 !false = true

// - -x 等价于 -(-x)
int val = - -5;  // 先算 -5,再算 -(-5) = 5

2.3 乘除模:数学课的「老三样」(第 3 级) ​

csharp
int a = 10, b = 3;

int product = a * b;   // 30(乘法)
int quotient = a / b;  // 3(整数除法,舍去小数)
int remainder = a % b; // 1(取余)

// 混合运算:乘除模同级,左结合
int result = a * b / 2;  // (10 * 3) / 2 = 30 / 2 = 15
int result2 = a / b * 2; // (10 / 3) * 2 = 3 * 2 = 6(注意:不是 10 / 6)

易错点:整数除法的截断效应

csharp
// ❌ 预期 3.33,实际 3
double wrong = 10 / 3;      // int / int = int,结果为 3,再隐式转 double

// ✅ 正确:至少一个操作数是浮点
double right = 10.0 / 3;    // 3.333...
double right2 = 10 / 3.0;   // 3.333...
double right3 = (double)10 / 3;  // 3.333...

2.4 加减:最熟悉的陌生人(第 4 级) ​

csharp
int sum = 10 + 5;    // 15
int diff = 10 - 5;   // 5

// 与乘除模混合:乘除模优先
int result = 10 + 5 * 2;   // 10 + (5*2) = 20,不是 (10+5)*2=30
int result2 = 10 - 6 / 3;  // 10 - (6/3) = 8,不是 (10-6)/3=1

经典 Bug 案例:

csharp
// 场景:计算平均分
int total = 80 + 90 + 70;
int count = 3;

// ❌ 错误:先除后加?不,是 total / count,但 240/3=80,碰巧对
// 但如果表达式是 80 + 90 + 70 / 3 呢?
int wrong = 80 + 90 + 70 / 3;   // 80 + 90 + 23 = 193(错误!)

// ✅ 正确:加括号明确意图
int correct = (80 + 90 + 70) / 3;  // 240 / 3 = 80

2.5 移位运算符:二进制世界的「推拉门」(第 5 级) ​

csharp
int x = 1;  // 二进制:00000001

int left = x << 2;   // 左移 2 位:00000100 = 4(相当于 ×4)
int right = x >> 1;  // 右移 1 位:00000000 = 0(相当于 ÷2)

// 与加减混合:加减优先于移位
int result = 1 << 2 + 1;   // 1 << (2+1) = 1 << 3 = 8
                           // 不是 (1<<2)+1 = 5!

// ✅ 正确写法
int correct = 1 << (2 + 1);  // 明确先算 2+1

2.6 关系比较:大小之辨(第 6 级) ​

csharp
int a = 5, b = 10;

bool less = a < b;      // true
bool greater = a > b;   // false
bool lessEq = a <= 5;   // true
bool greaterEq = b >= 10;  // true

// is 和 as(类型判断)
object obj = "hello";
bool isString = obj is string;     // true
string str = obj as string;        // "hello"(转换失败返回 null)

// 与算术混合:算术优先
bool result = a + 1 < b;  // (5+1) < 10 → 6 < 10 → true
                          // 不是 5 + (1<10),那会是类型错误

2.7 相等比较:是否相同(第 7 级) ​

csharp
int x = 5, y = 5;

bool equal = x == y;     // true
bool notEqual = x != y;  // false

// 与关系比较混合:关系比较优先
bool result = x < y == true;  // (x<y) == true → false == true → false
                              // 不是 x < (y==true),类型错误

// ✅ 正确:加括号明确
bool correct = (x < y) == true;

2.8 位运算符:二进制层的「开关大师」(第 8-10 级) ​

csharp
// & 位与:两个都为 1 才为 1
int a = 0b_1100;  // 12
int b = 0b_1010;  // 10
int and = a & b;  // 0b_1000 = 8

// ^ 位异或:不同为 1,相同为 0
int xor = a ^ b;  // 0b_0110 = 6

// | 位或:有一个为 1 就为 1
int or = a | b;   // 0b_1110 = 14

// 优先级:& > ^ > |
int result = a | b & a ^ b;  // 等价于 a | ((b & a) ^ b)
// 步骤:b & a = 0b_1000 = 8
//       8 ^ b = 0b_1000 ^ 0b_1010 = 0b_0010 = 2
//       a | 2 = 0b_1100 | 0b_0010 = 0b_1110 = 14

2.9 逻辑运算符:真假世界的「裁判」(第 11-12 级) ​

csharp
bool t = true, f = false;

// && 逻辑与:短路求值,左边 false 则右边不执行
bool and = t && f;  // false

// || 逻辑或:短路求值,左边 true 则右边不执行
bool or = t || f;   // true

// 与位运算符混合:位运算优先
bool result = t | f && f;  // t | (f && f) = t | false = true
                           // 不是 (t|f) && f = true && false = false

// ⚠️ 重要:& 和 &&、| 和 || 的区别
int x = 0;
bool check1 = (x != 0) && (10 / x > 1);  // 安全:x=0 时右边不执行
// bool check2 = (x != 0) & (10 / x > 1);   // 危险!右边会执行,除零异常

易错点:& vs &&

csharp
int divisor = 0;

// ❌ 危险:& 不短路,右边会执行,抛出 DivideByZeroException
// bool crash = (divisor != 0) & (100 / divisor > 5);

// ✅ 安全:&& 短路,左边 false 时右边不执行
bool safe = (divisor != 0) && (100 / divisor > 5);  // false,无异常

2.10 Null 合并运算符 ??:空值的「备胎」(第 13 级,右结合) ​

csharp
string a = null;
string b = "B";
string c = "C";

// ?? 右结合:从右到左
string result = a ?? b ?? c;  // 等价于 a ?? (b ?? c)
// 步骤:b ?? c = "B"(b 非 null),a ?? "B" = "B"

// ⚠️ 易错:?? 与 ?: 的优先级混淆
// string wrong = a ?? b == null ? "空" : "非空";  // 编译错误!

// ✅ 正确:加括号明确
string correct = (a ?? b) == null ? "空" : "非空";

2.11 条件运算符 ?::三岔路口(第 14 级,右结合) ​

csharp
int score = 85;

// 基础用法
string grade = score >= 60 ? "及格" : "不及格";

// 嵌套:右结合
string level = score >= 90 ? "优秀" :
               score >= 80 ? "良好" :
               score >= 60 ? "及格" : "不及格";
// 等价于:score >= 90 ? "优秀" : (score >= 80 ? "良好" : (score >= 60 ? "及格" : "不及格"))

// 与 ?? 混合:?? 优先级更高
string a = null;
string result = a ?? score >= 60 ? "有成绩" : "无成绩";
// 解析:a ?? (score >= 60 ? "有成绩" : "无成绩")
// 不是 (a ?? score) >= 60,类型不匹配

2.12 赋值与 Lambda:最后的「执行者」(第 15 级,右结合) ​

csharp
int x, y, z;

// 赋值右结合:从右到左
x = y = z = 100;  // z = 100 → y = 100 → x = 100

// Lambda =>
Func<int, int> square = n => n * n;  // => 优先级最低

// 与三元混合:三元优先
int result = true ? x = 1 : y = 2;   // 等价于 true ? (x=1) : (y=2)
// x = 1, y 未赋值(编译器可能报错,因为 y 可能未赋值)

三、优先级记忆模块 ​

3.1 记忆口诀 ​

点调索引新类型,一元正负非反增;
乘除模后加减移,大小相等位与异;
位或逻辑与或空,条件赋值 lambda。

逐句解释:

  1. 点调索引新类型:.、()、[]、new、typeof(第 1 级)
  2. 一元正负非反增:+、-、!、~、++、--(第 2 级)
  3. 乘除模后加减移:* / % → + - → << >>(第 3-5 级)
  4. 大小相等位与异:< > <= >= → == != → & ^(第 6-9 级)
  5. 位或逻辑与或空:| → && → || → ??(第 10-13 级)
  6. 条件赋值 lambda:?: → = += =>(第 14-15 级)

3.2 通用避坑准则 ​

除了数学运算的基础优先级,其他复杂混合运算一律加括号。

csharp
// ❌ 依赖记忆:容易出错
int result = a + b * c << 2 & 0xFF;

// ✅ 明确意图:一目了然
int result = ((a + (b * c)) << 2) & 0xFF;

四、实战测试:你能算对吗? ​

测试 1:基础混合运算 ​

csharp
int a = 2, b = 3, c = 4;
int result = a + b * c - a * b;
点击查看解析

解析:

  • * 优先:b * c = 12,a * b = 6
  • + 和 - 同级,左结合:(2 + 12) - 6 = 8

答案:8

测试 2:移位与位运算 ​

csharp
int x = 1;
int result = x << 2 + 1 | 0b_0010;
点击查看解析

解析:

  • + 优先于 <<:2 + 1 = 3
  • <<:1 << 3 = 8(二进制 1000)
  • |:8 | 2 = 10(1000 | 0010 = 1010)

答案:10

正确写法:((x << (2 + 1)) | 0b_0010)

测试 3:逻辑与短路 ​

csharp
int count = 0;
bool Check() { count++; return true; }

bool result = false && Check() || true;
Console.WriteLine(count);
点击查看解析

解析:

  • && 优先于 ||:false && Check() → 短路,Check() 不执行,返回 false
  • ||:false || true → true
  • count 未被修改,输出 0

答案:count = 0

测试 4:?? 与 ?: 的优先级陷阱 ​

csharp
string a = null;
string result = a ?? "default" == null ? "空" : "非空";
点击查看解析

解析:

  • ?? 优先级低于 ==,但高于 ?:
  • 实际解析:a ?? ("default" == null ? "空" : "非空")
  • "default" == null → false
  • false ? "空" : "非空" → "非空"
  • a ?? "非空" → "非空"

答案:"非空"

正确写法:(a ?? "default") == null ? "空" : "非空"

测试 5:复合赋值与自增 ​

csharp
int i = 5;
i += i++ + ++i;
Console.WriteLine(i);
点击查看解析

解析:

  • i++:后缀,取 5,i 变 6
  • ++i:前缀,i 变 7,取 7
  • i++ + ++i = 5 + 7 = 12
  • i += 12:i = 7 + 12 = 19

答案:19

注意:这是未定义行为(C# 规范不保证求值顺序),绝对不要在生产代码中这样写。


五、总结:优先级的「黄金法则」 ​

  1. 数学运算记基本:* / % > + -,同级左结合
  2. 复杂运算加括号:不确定就加括号,括号是最好的文档
  3. 短路逻辑用 && ||:避免除零、空引用等运行时异常
  4. ?? 和 ?: 是右结合:嵌套时从右往左读
  5. 前缀后缀要分清:++i 先变后用,i++ 先用后变

掌握这些规则,你就能像裁判官一样,精准判断每一行代码的执行顺序,彻底告别「算错账」的 Bug。