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 优先级速查总表
| 优先级 | 类别 | 运算符 | 结合性 |
|---|---|---|---|
| 1 | Primary 基础 | 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 | 逻辑或 | || | 左 |
| 13 | Null 合并 | ?? | 右 |
| 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=72.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) = 52.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 = 802.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+12.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 = 142.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。逐句解释:
- 点调索引新类型:
.、()、[]、new、typeof(第 1 级) - 一元正负非反增:
+、-、!、~、++、--(第 2 级) - 乘除模后加减移:
*/%→+-→<<>>(第 3-5 级) - 大小相等位与异:
<><=>=→==!=→&^(第 6-9 级) - 位或逻辑与或空:
|→&&→||→??(第 10-13 级) - 条件赋值 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→truecount未被修改,输出0
答案:count = 0
测试 4:?? 与 ?: 的优先级陷阱
csharp
string a = null;
string result = a ?? "default" == null ? "空" : "非空";点击查看解析
解析:
??优先级低于==,但高于?:- 实际解析:
a ?? ("default" == null ? "空" : "非空") "default" == null→falsefalse ? "空" : "非空"→"非空"a ?? "非空"→"非空"
答案:"非空"
正确写法:(a ?? "default") == null ? "空" : "非空"
测试 5:复合赋值与自增
csharp
int i = 5;
i += i++ + ++i;
Console.WriteLine(i);点击查看解析
解析:
i++:后缀,取 5,i 变 6++i:前缀,i 变 7,取 7i++ + ++i=5 + 7=12i += 12:i = 7 + 12 = 19
答案:19
注意:这是未定义行为(C# 规范不保证求值顺序),绝对不要在生产代码中这样写。
五、总结:优先级的「黄金法则」
- 数学运算记基本:
*/%>+-,同级左结合 - 复杂运算加括号:不确定就加括号,括号是最好的文档
- 短路逻辑用
&&||:避免除零、空引用等运行时异常 ??和?:是右结合:嵌套时从右往左读- 前缀后缀要分清:
++i先变后用,i++先用后变
掌握这些规则,你就能像裁判官一样,精准判断每一行代码的执行顺序,彻底告别「算错账」的 Bug。