Rust引用、可变引用及其编译错误解析

引用

概念

上节中提到,Rust中,无Copy Trait的数据类型的函数传参,等效于所有权的转移。因此当函数退出时,接收所有权的变量作用域结束,数据就被销毁。那么,是否有办法让函数传参不发生所有权的转移?

有的!

Rust提供了被称为“引用”的机制。如下例:

1
2
3
4
5
6
7
8
9
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
s.len()
}

以上程序可以被正常运行,变量s不会在离开自己的作用域时销毁其指向的数据,因为它并不拥有数据的所有权。

引用的常见编译错误

错误1
1
2
3
4
5
6
7
8
fn main() {
    let s = String::from("hello");
    append_string(&s);


fn append_string(s: &String) {
    s.push_str(" world");
}

编译结果:

1
2
3
4
5
6
7
8
9
10
$ cargo build
Compiling hello v0.1.0 (/home/spencer/share/my_code/rust_prj/hello)
error[E0596]: cannot borrow `*s` as mutable, as it is behind a `&` reference
--> src/main.rs:14:5
|
13 | fn append_string(s: &String) {
| ------- help: consider changing this to be a mutable reference: `&mut String`
14 | s.push_str(" world");
| ^ `s` is a `&` reference, so the data it refers to cannot be borrowed as mutable

解析:引用是默认是不可变的,修改引用变量指向的数据是被禁止的。

可变引用

概念

Rust允许我们使用mut将某个引用声明为“可变引用”

1
2
3
4
5
6
7
8
9
fn main() {
    let mut s = String::from("hello");
    append_string(&mut s);
    println!("{}", s);
}

fn append_string(some_string: &mut String) {
    some_string.push_str(", world");
}

以上程序可以被正常执行,append_string函数的入参类型被指定为&mut String,即可变引用的String类型。append_string中完成了对入参的修改。

Rust以数据访问安全著称。那么它是如何在支持可变引用的情况下,仍然可以避免数据的竞争的?大家可以通过以下编译错误的例子体会一下。

可变引用常见的编译错误

错误2
1
2
3
4
5
6
fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut s;
    println!("{}, {}", r1, r2);
}

编译结果 :

1
2
3
4
5
6
7
8
9
10
11
   Compiling hello v0.1.0 (/home/spencer/share/my_code/rust_prj/hello)
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{}, {}", r1, r2);
| -- first borrow later used here

解析:以上例子中,r1和r2都为s的可变引用,且在同一个作用域中。Rust编译不不允许这样的代码存在。因为r1r2可以在同一个作用域操作同一份数据,会产生数据竞争。

当我们用大括号将r1的作用域加以限制之后,r1r2的作用域不再重叠,就可以编译通过了。如下:

1
2
3
4
5
6
7
fn main() {
    let mut s = String::from("hello");
    {
        let r1 = &mut s;
    }
    let r2 = &mut s;
}
错误3
1
2
3
4
5
6
7
fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    s.push_str(" world");
    r1.push_str(" hello2");
    println!("{}", s);
}

编译结果 :

1
2
3
4
5
6
7
8
9
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:4:5
|
3 | let r1 = &mut s;
| ------ first mutable borrow occurs here
4 | s.push_str(" world");
| ^ second mutable borrow occurs here
5 | r1.push_str(" hello2");
| -- first borrow later used here

解析:与上一个错误类似,同样也出现了“second mutable borrow occurs here”的提示。而发生此错误的地方,是第4行使用原始变量s做了数据修改的操作。编译器将使用原始可变变量做的数据修改,视为另一种可变引用。于是,sr1产生了数据竞争,编译失败。

更确切地说,在以下三个条件同时满足时,会产生数据竞争,发出编译错误:

  • 两个或两个以上的pointer(包含所有者,可变引用)指向同一份数据。
  • 其中至少一个可变引用指会向空间写入数据。
  • 没有同步数据的访问机制。

引用的原则:

  • 任何时刻,一个变量只能有
    • 一个可变引用,或者
    • 多个不可变引用
    • 以上两点不可同时存在
  • 引用应该总是合法的