借用检查器的惊喜
4星 - Rust借用检查器的5个令人困惑的行为
作者在编写借用检查器的过程中发现,Rust的实际行为经常与他的心智模型不符。他发现很多有经验的Rust开发者也对这些细节感到困惑。
一、表达式求值顺序
fn main() {
let mut x = 0;
let y = &mut x;
*y = *y + 1;
}
这个代码居然能编译通过!直觉上,我们认为左侧赋值表达式会先产生一个可变引用,然后右侧读取 x,这应该是不允许的。
答案: 右侧表达式先求值,所以右侧先读取 x,然后左侧才产生可变引用。
二、双阶段借用(Two-Phase Borrows)
use std::ops::AddAssign;
fn main() {
let mut x = 0;
x += x; // OK
}
这没问题。但如果我们反糖化(desugar):
use std::ops::AddAssign;
fn main() {
let mut x = 0;
AddAssign::add_assign(&mut x, x); // Error!
}
Rust有一个名为双阶段借用的特性,只在特定情况下激活——比如 . 方法调用语法。
第一个 x 最初被当作不可变引用,然后其他参数被求值,最后"激活"为可变引用。这个特性在反糖化版本中不会激活。
三、隐式重借用(Implicit Reborrow)
fn id(y: &mut usize) -> &mut usize {
y
}
fn main() {
let mut x = 0;
let y = &mut x;
let z = id(y);
*y = 1; // 竟然可以!
}
id(y) 实际上被反糖化为隐式重借用 id(&mut *y)。所以 y 本身没有被移动,z 包含一个从 y 重借用的新引用。z 从未被使用,所以这个引用立即被销毁,y 再次可用。
四、返回借用与生命周期
fn foo(a: &mut usize) -> &mut usize {
let b = &mut *a;
let c = &mut *b;
return c // 这居然是合法的!
}
c 的生命周期与 a 相同,所以可以从函数返回,即使它派生自 b 而 b 会在作用域结束时被销毁。
但如果我们显式调用 drop:
fn foo(a: &mut usize) -> &mut usize {
let b = &mut *a;
let c = &mut *b;
drop(b); // Error!
return c
}