使用以下代码,我对借用投诉感到惊讶
#[derive(Debug)]
struct ExternalStruct {
some_thing: i32,
}
impl ExternalStruct {
fn non_mutable_read(&self) {
println!("{}", self.some_thing);
}
fn create(&mut self) -> MyStruct {
self.some_thing += 1;
MyStruct{external_value: self}
}
}
#[derive(Debug)]
struct MyStruct<'a> {
external_value: &'a ExternalStruct,
}
fn main() {
let mut ext = ExternalStruct { some_thing: 18 };
let my = ext.create();
ext.non_mutable_read();
println!("{:?}", my);
}
错误
error[E0502]: cannot borrow `ext` as immutable because it is also borrowed as mutable
--> src\main.rs:26:5
|
25 | let my = ext.create();
| --- mutable borrow occurs here
26 | ext.non_mutable_read();
| ^^^ immutable borrow occurs here
27 | println!("{:?}", my);
| -- mutable borrow later used here
如果 MyStruct 持有ExternalStruct 的 &mut,我就会理解这个错误。 但由于它具有不可变性,我误解了借用失败的原因。
对我来说,可变借用发生在 create 内部,并在 create 结束时完成(自身的可变借用)。如果创建的对象不包含 self 的引用,则情况如此。对于 Mystruct 对象,我认为它启动了一个不可变的借用,它将与 non_mutable_read 的第二个不可变借用兼容。
来自优秀的常见的 Rust 终身误解:
9)将 mut 引用降级为共享引用是安全的
误解推论
- 重新借用引用会结束其生命周期并开始新的生命周期
您可以将 mut 引用传递给需要共享引用的函数,因为 Rust 会隐式地重新借用 mut 引用作为不可变的:
fn takes_shared_ref(n: &i32) {} fn main() { let mut a = 10; takes_shared_ref(&mut a); // ✅ takes_shared_ref(&*(&mut a)); // above line desugared }
直观上这是有道理的,因为重新借用一个不可变的 mut 引用没有什么坏处,对吗?令人惊讶的是没有,因为下面的程序无法编译:
fn main() { let mut a = 10; let b: &i32 = &*(&mut a); // re-borrowed as immutable let c: &i32 = &a; dbg!(b, c); // ❌ }
抛出此错误:
error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable --> src/main.rs:4:19 | 3 | let b: &i32 = &*(&mut a); | -------- mutable borrow occurs here 4 | let c: &i32 = &a; | ^^ immutable borrow occurs here 5 | dbg!(b, c); | - mutable borrow later used here
可变借用确实发生了,但它会立即无条件地重新借用为不可变的,然后被删除。为什么 Rust 将不可变的重新借用视为仍然具有 mut ref 的独占生命周期?虽然上面的特定示例没有问题,但允许将 mut 引用降级为共享引用的能力确实会引入潜在的内存安全问题:
use std::sync::Mutex; struct Struct { mutex: Mutex<String> } impl Struct { // downgrades mut self to shared str fn get_string(&mut self) -> &str { self.mutex.get_mut().unwrap() } fn mutate_string(&self) { // if Rust allowed downgrading mut refs to shared refs // then the following line would invalidate any shared // refs returned from the get_string method *self.mutex.lock().unwrap() = "surprise!".to_owned(); } } fn main() { let mut s = Struct { mutex: Mutex::new("string".to_owned()) }; let str_ref = s.get_string(); // mut ref downgraded to shared ref s.mutate_string(); // str_ref invalidated, now a dangling pointer dbg!(str_ref); // ❌ - as expected! }
这里的要点是,当您重新借用 mut ref 作为共享引用时,您不会在没有大问题的情况下获得该共享引用:它会在重新借用期间延长 mut ref 的生命周期,即使 mut ref本身被丢弃。使用重新借用的共享引用非常困难,因为它是不可变的,但它不能与任何其他共享引用重叠。重新借用的共享引用具有 mut 引用和共享引用的所有缺点,但没有两者的优点。我认为重新借用 mut 引用作为共享引用应该被视为 Rust 反模式。意识到这种反模式很重要,这样当您看到如下代码时就可以轻松发现它:
// downgrades mut T to shared T fn some_function<T>(some_arg: &mut T) -> &T; struct Struct; impl Struct { // downgrades mut self to shared self fn some_method(&mut self) -> &Self; // downgrades mut self to shared T fn other_method(&mut self) -> &T; }
即使你避免在函数和方法签名中重新借用,Rust 仍然会自动隐式重新借用,因此很容易遇到这个问题而没有意识到,如下所示:
use std::collections::HashMap; type PlayerID = i32; #[derive(Debug, Default)] struct Player { score: i32, } fn start_game(player_a: PlayerID, player_b: PlayerID, server: &mut HashMap<PlayerID, Player>) { // get players from server or create & insert new players if they don't yet exist let player_a: &Player = server.entry(player_a).or_default(); let player_b: &Player = server.entry(player_b).or_default(); // do something with players dbg!(player_a, player_b); // ❌ }
以上无法编译。
返回一个or_default()
,由于我们显式的类型注释,我们隐式地将其重新借用为&mut Player
。为了做我们想做的事,我们必须:&Player
use std::collections::HashMap; type PlayerID = i32; #[derive(Debug, Default)] struct Player { score: i32, } fn start_game(player_a: PlayerID, player_b: PlayerID, server: &mut HashMap<PlayerID, Player>) { // drop the returned mut Player refs since we can't use them together anyway server.entry(player_a).or_default(); server.entry(player_b).or_default(); // fetch the players again, getting them immutably this time, without any implicit re-borrows let player_a = server.get(&player_a); let player_b = server.get(&player_b); // do something with players dbg!(player_a, player_b); // ✅ }
有点尴尬和笨重,但这是我们在内存安全祭坛上做出的牺牲。
要点
- 尽量不要将 mut 引用重新借用为共享引用,否则你会过得很糟糕
- 重新借用 mut 引用并不会结束其生命周期,即使引用被删除