为什么这个不可变的引用字段持有可变的借用

问题描述 投票:0回答:1

使用以下代码,我对借用投诉感到惊讶

#[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 borrow-checker
1个回答
0
投票

来自优秀的常见的 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 引用并不会结束其生命周期,即使引用被删除
© www.soinside.com 2019 - 2024. All rights reserved.