Skip to content

13.1 编译错误示例

Rust 的编译错误列表中,从 E0499E0509,所有的这些编译错误,其实都在讲同一件事情。它们主要关心的是共享和可变之间的关系。“共享不可变,可变不共享”是所有这些编译错误遵循的同样的法则。

下面我们通过几个简单的示例来直观地感受一下这个规则究竟是什么意思。

示例一

rust
fn main() {
    let i = 0;
    let p1 = & i;
    let p2 = & i;
    println!("{} {} {}", i, p1, p2);
}

以上这段代码是可以编译通过的。其中变量绑定ip1p2指向的是同一个变量,我们可以通过不同的 Path 访问同一块内存p*p1*p2,所以它们存在“共享”。 而且它们都只有只读的权限,所以它们存在“共享”,不存在“可变”。因此它一定是安全的。

示例二

我们让变量绑定i是可变的,然后在存在p1的情况下,通过i修改变量的值:

rust
fn main() {
    let mut i = 0;
    let p1 = & i;
    i = 1;
    println!("{}", p1)
}

编译,出现了错误,错误信息为:

txt
error[E0506]: cannot assign to `i` because it is borrowed

这个错误可以这样理解:在存在只读借用的情况下,变量绑定ip1已经互为 alias,它们之间存在“共享”,因此必须避免“可变”。这段代码违反了“共享不可变”的原则。

示例三

如果我们把上例中的借用改为可变借用的话,其实是可以通过它修改原来变量的值的。以下代码可以编译通过:

rust
fn main() {
    let mut i = 0;
    let p1 = &mut i;
    *p1 = 1;
    println!("{}", p1)
}

那我们是不是说,它违反了“共享不可变”的原则呢?其实不是。因为这段代码中不存在“共享”。

在可变借用存在的时候,编译器认为原来的变量绑定i已经被冻结(frozen),不可通过i读写变量。此时有且仅有p1这一个入口可以读写变量。 证明如下:

rust
fn main() {
    let mut i = 0;
    let p1 = &mut i;
    *p1 = 1;
    let x = i; // 通过 i 读变量
    println!("{}", p1)
}

在存在p1的情况下,我们再通过i做读操作是错误的:

error[E0503]: cannot use `i` because it was mutably borrowed

同理,如果我们改成下面这样,一样会出错:

rust
fn main() {
    let mut i = 0;
    let p1 = &mut i;
    i = 1; // 通过 i 写变量
    p1;
}

p1存在的情况下,不可通过i写变量。如果这种情况可以被允许,那就会出现多个入口可以同时访问同一块内存,且都具有写权限,这就违反了 Rust 的“共享不可变,可变不共享”的原则。 错误信息为:

error[E0506]: cannot assign to `i` because it is borrowed

示例四

同时创建两个可变借用的情况:

rust
fn main() {
    let mut i = 0;
    let p1 = &mut i;
    let p2 = &mut i;
    p1;
}

编译错误信息为:

txt
error: cannot borrow `i` as mutable more than once at a time [E0499]

因为p1p2都是可变借用,它们都指向了同一个变量,而且都有修改权限,这是 Rust 不允许的情况,因此这段代码无法编译通过。

正因如此,&mut型借用也经常被称为“独占指针”,&型借用也经常被称为“共享指针”

官方示例

下面的示例来自:https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html

不可变引用的用户可不希望在他们的眼皮底下值就被意外的改变了!多个不可变引用是可以的,因为只能读取数据的人是没有能力影响其他人读取到的数据。

所以下面的代码会报错:

rust
fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{}, {}, and {}", r1, r2, r3);
}

由于 NLL 的存在,引用的生命周期从它被声明的地方开始一直持续到它最后一次被使用的地方而非作用域结尾(注意主语是生命周期,存在依赖关系的引用拥有同一个生命期)。 注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。 例如,因为最后一次使用不可变引用(println!中),发生在声明可变引用之前,所以如下代码是可以编译的:

rust
let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// variables r1 and r2 will not be used after this point

let r3 = &mut s; // no problem
println!("{}", r3);

不可变引用r1r2的作用域与可变引用r3的作用域没有重叠,所以代码是可以编译的。编译器可以在作用域结束之前判断不再使用的引用。

Released under the MIT License