写Rust的人里有83%自认懂所有权系统,但真拿这几段代码考他们,能全答对的不到两成。这不是我编的——作者Jamie Brandon在写自己的借用检查器时,反复对照Rust官方行为,发现连资深开发者都会给出"自信但错误"的解释。
借用检查器的黑箱,比多数人想象的更深。
Brandon做了件狠事:把Rust编译器的"意外行为"做成互动测验。每道题先看代码自己推理,再点揭晓。结果?"一旦看到答案,你会觉得自己早就知道"——这种认知错觉正是陷阱所在。
第一题:同一行里先读再写,凭什么能过?
代码极简:
let mut x = 0;let y = &mut x;*y = *y + 1;
左边`*y`是赋值目标,需要可变引用;右边`*y + 1`要从`x`读取。按直觉,可变借用和不可变借用不能共存,这行应该报错。
但编译通过了。为什么?
Brandon用显式代码块验证了求值顺序:
*({println!("lhs"); y}) = {println!("rhs"); *y + 1};输出是`rhs`先,`lhs`后。右侧表达式先求值,此时`x`还未被可变借用,读取合法;左侧后求值,才建立可变引用。时间差救了这行代码。
第二题:`+=`的糖衣剥完,编译器翻脸了
这题更刁。起点 innocuous:
let mut x = 0;x += x;
显然能过。但`+=`是语法糖,展开成`AddAssign::add_assign`方法调用:
x.add_assign(x);
还能过。再剥一层:方法调用也是糖,实际是普通函数调用`add_assign(&mut x, x)`。
这次编译器炸了:
error[E0503]: cannot use `x` because it was mutably borrowed
同一个语义,三层语法糖,两层能过一层报错。Rust有个叫"两阶段借用"(two-phase borrows)的隐藏机制,只在特定场景触发——方法调用语法`.`是其中之一,普通函数调用不是。
机制细节:第一个`x`先被当作不可变引用捕获,等其他参数求值完,再"升级"成可变引用。剥完糖衣变成函数调用,这个特权就消失了。
第三题:Move语义的老朋友
最后一题是送分题,但放在这里别有用心:
let z = y;*y = 1; // 报错:use of moved value
可变引用`&mut T`不实现`Copy` trait,`y`被move给`z`后自然失效。这题没人会错,但 Brandon 的用意明显:前面两题的"意外"之所以难察觉,恰恰因为Rust大部分时候确实如你预期,麻痹了警觉。
三题看完,一个模式浮现:Rust的借用规则在纸面上简洁,实际执行却依赖求值顺序、语法糖展开、隐藏机制等未言明的实现细节。这些不是bug,是设计权衡——但文档没告诉你,编译器报错也不会解释。
Brandon 的测验在GitHub和 lobste.rs 引发大量讨论。有开发者承认"第三题我以为懂,前两题全靠猜";也有人反驳"这些边缘案例不值得关心"。
争议本身说明问题:Rust社区长期宣传"编译器即文档",但编译器的实际行为与用户的 mental model 之间存在系统性的信息缺口。这个缺口不会阻止你写代码,但会在你调试生命周期错误时,让每一分钟都变得更漫长。
Brandon 最后没给结论,只留了个开放问题:如果你正在设计一门新语言,会为了用户直觉的简洁性,牺牲这些实现层面的优化空间吗——还是说,正确的答案从来不在二选一?
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.