Rust 运算符优先级:括号不只用来改变顺序

一、先把这段跑一遍

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 里靠警告提示的链式比较变成了编译错误,把比较与位运算的顺序调成了更符合直觉的方向,同时用「比较与范围必须加括号」和「赋值返回 ()」立了几条硬规矩。

记住第四条分界线就够了:优先级管结合、左到右管求值、短路管跳过、括号管歧义。

参考

相关推荐
写码跑山的老艾11 分钟前
为什么有了 MySQL,还要 Redis 和 Elasticsearch?从一件商品看数据系统的权衡
后端
对象存储与RustFS17 分钟前
Jenkins 的构建产物别堆在控制器磁盘:用 RustFS 当它的 S3 工件仓库
后端·docker·云原生
王中阳Go18 分钟前
100 件货卖出 116 件,库存停在 -16
后端·mysql·go
泡海椒19 分钟前
jquick-pdf 多重折线(JMultiLineChartData)实战:Java 生成工单 SLA 月报
后端
Swot21 分钟前
SvelteKit Remote Functions 使用指南
前端·后端
Java旅途28 分钟前
让项目过程清晰可追溯:从课题双选到文档归档的一体化平台
后端
黄连木笛30 分钟前
不改 Redis Insight 原有代码,把它汉化了:DOM 整段匹配 + XHR 拦截 + asar 重打包
redis·后端
flash俊杰31 分钟前
v2.34-02 静默截断:10,000 个会话与 500 条消息是怎么被发现的
后端
vaskilab40 分钟前
Agent 上线后翻车的 6 种死法:一个 20 万人规模项目的四版重构实录
后端