一、先把这段跑一遍
rust
println!("{}", 2 + 3 * 4); // 14:先 3*4=12,再 2+12
println!("{}", (2 + 3) * 4); // 20:先 2+3=5,再 5*4
println!("{}", 2 + 3 * 4); // 14
println!("{}", 2 + (3 * 4)); // 14,但意图更清楚
实测输出 14 / 20 / 14 14,:乘除优先于加减、括号强制改变顺序、拿不准就加括号。
二、完整优先级表(从强到弱)
The Rust Reference · Expression precedence 给出的顺序:
| 层级(强 → 弱) | 结合性 |
|---|---|
路径、方法调用、字段访问、函数调用、数组索引、? |
左到右 |
一元 - ! *、借用 & &mut |
左到右 |
as |
左到右 |
* / % |
左到右 |
+ - |
左到右 |
<< >> |
左到右 |
& → ^ → ` |
`(三者逐级降低) |
== != < > <= >= |
Require parentheses |
&& → ` |
|
.. ..= |
Require parentheses |
| `= += -= *= /= %= &= | = ^= <<= >>=` |
return、break、闭包 |
--- |
表里两个 Require parentheses 最值得记住:比较运算符和范围运算符不允许链式。这是语法层面的限制,不是风格建议。
同级结合性也别靠猜,实测:
ini
10 - 3 - 2 = 5 100 / 10 / 2 = 5 2 * 3 % 4 = 2
都是左结合。赋值虽然右结合,但赋值表达式的值是 () (实测 (a = 5) 打印出来是 ()),所以 a = b = 5 在 Rust 里根本不成立:
ini
error[E0308]: mismatched types
4 | a = b = 5;
| ^^^^^ expected integer, found `()`
C/Java 里链式赋值是常规写法,转过来会撞一下。
三、和 C 相反的一条规则:比较低于位运算
表里 & ^ | 排在比较运算符之上,也就是说 Rust 里位运算结合得更紧。实测:
rust
println!("{}", 6 & 3 == 2); // true,解析为 (6 & 3) == 2
同样的 C 代码:
c
printf("%d\n", 6 & 3 == 2); // 0,解析为 6 & (3 == 2)
ini
warning: & has lower precedence than ==; == will be evaluated first [-Wparentheses]
6 & 3 == 2 -> 0
这是 C 的经典坑,Rust 把它反过来了,flags & MASK == 0 恰好符合直觉。但我仍然建议写成 (flags & MASK) == 0 ------ 不是为了迁就编译器,而是为了迁就读代码的人:团队里从 C/Java 过来的人看到裸写法会本能地停顿一下,括号把这个停顿消掉。
Rust 更狠的地方在链式比较。下面这行直接编译不过:
go
error: comparison operators cannot be chained
2 | println!("{}", 1 < 2 < 3);
| ^ ^
help: split the comparison into two
| println!("{}", 1 < 2 && 2 < 3);
而 C 里同一句合法,结果是 1((1 < 2) < 3 → 1 < 3),clang 只能给一条警告:
vbnet
warning: comparisons like 'X<=Y<=Z' don't have their mathematical meaning
1 < 2 < 3 -> 1
范围同理,0..1..2 会报 expected one of ',', '.', '?', or an operator, found '..'。Rust 选择把这类"看起来像数学、实际不是"的写法变成编译错误,这比事后调试舒服得多。
四、优先级 ≠ 求值顺序
这是最容易被混为一谈的两件事。Reference 说得很清楚:
Which subexpressions are the operands of an expression is determined by expression precedence. Expressions taking multiple operands are evaluated left to right as written in the source code.
优先级决定谁和谁结合,求值顺序决定副作用何时发生。 实测这一段:
rust
fn f(n: i32) -> i32 { println!(" eval f({})", n); n }
let r = f(1) + f(2) * f(3);
scss
eval f(1)
eval f(2)
eval f(3)
result = 7
调用顺序严格按源码从左到右 f(1) → f(2) → f(3),尽管最后算的是 1 + (2 * 3)。别把"乘法先算"理解成"f(2) 和 f(3) 先被调用"。
短路则是第三件事------某些操作数可能根本不求值。实测:
sql
side(false, "L") && side(true, "R") → 只打印 call L,结果 false
side(false, "L") & side(true, "R") → 打印 call L 和 call R,结果 false
&& / || 短路,& / | 是按位运算,两侧都会求值。左侧有副作用或昂贵计算时,两者不能互换。
范围运算符优先级很低,混着算术写要注意:实测 0..10 + 1 得到 0..11,即 0..(10 + 1)。写成 0..(n + 1) 更不容易被误读。
五、括号的第三种作用:救类型推导
说括号「改变顺序」和「让意图更清楚」,其实还有个更实际的用途。方法调用比一元负号结合更紧:
rust
let x = 1_i32;
println!("{} {}", -x.abs(), (-x).abs()); // -1 1
-x.abs() 是 -(x.abs()),想要「负数取绝对值」必须写 (-x).abs()。这类 bug 不会报错,只会静静给错结果。
as 和一元负号挨在一起时更微妙,实测四个写法:
| 写法 | 结果 |
|---|---|
-1 as u8 |
编译错误 E0600: cannot apply unary operator - to type u8 |
-1i32 as u8 |
255 |
(-1i32) as u8 |
255 |
-(1 as u8) |
编译错误 E0600 |
字面量有没有类型后缀会改变结果 ------无后缀的 1 被 as u8 先钉成 u8,一元负号就无处可去了;加上 i32 后缀后,整体按 (-1i32) as u8 走,得到 255。别在这里赌优先级表,写括号或写后缀。
顺带说一句:别指望工具链全兜住。本机 clippy 0.1.99 对 -x.abs() 没有给出 precedence 相关提示(我显式加了 -W clippy::precedence 也没触发)。「拿不准就加括号」是比 lint 更可靠的保险。
六、速查
| 场景 | 结论 |
|---|---|
| 同级运算 | 左结合:10 - 3 - 2 == 5;赋值右结合但值为 () |
| 位运算 + 比较 | Rust 是 (a & b) == c;C 是 a & (b == c),仍建议加括号 |
1 < 2 < 3 |
编译错误,写 1 < 2 && 2 < 3 |
0..1..2 |
编译错误,范围不能链式 |
a = b = 5 |
编译错误,赋值表达式返回 () |
0..10 + 1 |
0..11,范围优先级很低 |
f(1) + f(2) * f(3) |
调用顺序仍是从左到右 |
&& vs & |
前者短路,后者两侧都求值 |
-x.abs() |
是 -(x.abs()),不是 (-x).abs() |
-1 as u8 |
编译错误;用 -1i32 as u8 或 (-1i32) as u8 |
工程上就三条:混合位运算与比较一律加括号;&& / || 混用加括号;遇到一元负号、方法调用、as 相邻时加括号或写类型后缀。括号不增加运行时开销,只减少读者和编译器的分歧。
小结
Rust 的优先级表不只决定"先算谁",还和结合性、类型推导、以及"哪些写法根本不合法"绑在一起 。它把 C 里靠警告提示的链式比较变成了编译错误,把比较与位运算的顺序调成了更符合直觉的方向,同时用「比较与范围必须加括号」和「赋值返回 ()」立了几条硬规矩。
记住第四条分界线就够了:优先级管结合、左到右管求值、短路管跳过、括号管歧义。