可变引用
在上一篇文章中,我们提到了借用的概念,将获取引用作为函数参数称为 借用(borrowing),通常情况下,我们无法修改借来的变量
,但是可以通过可变引用实现修改借来的变量
。代码示例如下:
fn main() {let mut s = String::from("hello"); // s是可变的变量change(&mut s); // &mut 表示可变引用
}fn change(some_string: &mut String) { // &mut 表示可变引用some_string.push_str(", world");
}
要想实现修改借来的变量
就必须将 s
改为 mut
。然后必须创建一个可变引用 &mut s
和接受一个可变引用 some_string: &mut String
。
但是可变引用有一个很大的限制:在特定作用域中的特定数据只能有一个可变引用。比如下述代码就不会被成功编译。
fn main() {let mut s = String::from("hello"); let r1 = &mut s;let r2 = &mut s;
}
编译运行就会抛出如下异常:
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
所以这种修改借来的变量
的可变引用是以一种受限制的方式允许修改,这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争(data race)类似于竞态条件,它可由这三个行为造成:
- 两个或更多指针同时访问同一数据。
- 至少有一个指针被用来写入数据。
- 没有同步数据访问的机制。
数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生。我们可以使用{}
创建一个新的作用域,这样就能够允许多个可变引用了,只是不能在同一个作用域中同时拥有:
fn main() {let mut s = String::from("hello");{let r1 = &mut s;} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用let r2 = &mut s;
}
另外还需要注意的是,不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在他们的眼皮底下值就被意外的改变了!但是多个不可变引用是可以的,因为没有哪个只能读取数据的人有能力影响其他人读取到的数据。如下述代码:
fn main() {let mut s = String::from("hello");let r1 = &s; // 没问题let r2 = &s; // 没问题let r3 = &mut s; // 在拥有不可变引用的同时拥有可变引用println!("{}, {}, and {}", r1, r2, r3);}
上面代码示例编译时会抛出如下异常:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable--> src/main.rs:6:14|
4 | let r1 = &s; // no problem| -- immutable borrow occurs here
5 | let r2 = &s; // no problem
6 | let r3 = &mut s; // BIG PROBLEM| ^^^^^^ mutable borrow occurs here
7 |
8 | println!("{}, {}, and {}", r1, r2, r3);| -- immutable borrow later used here
但是如果可变引用和不可变引用他们的作用域不重叠代码就是可以编译的,我们可以将上面的代码示例进行修改就可以正常运行了。
fn main() {let mut s = String::from("hello");let r1 = &s; // 没问题let r2 = &s; // 没问题println!("{} and {}", r1, r2);// 此位置之后 r1 和 r2 不再使用let r3 = &mut s; // 没问题println!("{}", r3);}
悬垂引用(Dangling References)
在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
当我们不小心创建了悬垂引用,Rust在编译的时候就会抛出异常:
fn main() {let reference_to_nothing = dangle();
}fn dangle() -> &String { // dangle 返回一个字符串的引用let s = String::from("hello"); // s 是一个新字符串&s // 返回字符串 s 的引用
}// 这里 s 离开作用域并被丢弃。其内存被释放。
因为 s
是在 dangle
函数内创建的,当 dangle
的代码执行完毕后,s
将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的 String
,所以在编译时Rust就会抛出异常,解决方式就是直接返回String
。
fn no_dangle() -> String {let s = String::from("hello");s
} // 所有权被移动出去,内存没有被释放