来源: scattered-thoughts.net | Lobsters: 51 points | 标签: Rust/Programming Language Theory

借用检查器的惊喜

★★★★☆ 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
}

显式调用 drop 被当作普通函数处理,不会获得与隐式相同的特殊处理。

五、运算符重载与求值顺序

fn foo(x: &mut usize, y: usize) {
    *x += y
}

fn bar(x: &mut T, y: T) {
    *x += y
}

这两个函数看起来相同,但求值顺序不同:

差异来自 . 方法调用语法和泛型函数的不同处理方式。这是 Rust 难以学习的部分原因:核心借用检查规则很简单,但因为人体工程学原因有太多例外。


总结

虽然核心借用检查规则很简单,但 Rust 有很多特殊情况和例外,这使得开发者很容易内化错误的规则。这些细节对于深入理解 Rust 至关重要。

作者: jamii | 来源: scattered-thoughts.net