Почему это неизменяемое поле ссылки содержит изменяемый заимствование

В следующем коде я был удивлен жалобой на заимствование

#[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 содержал &mut ExternalStruct. Но поскольку он содержит неизменяемую информацию, я неправильно понимаю, почему заимствование не удалось.

Для меня изменяемое заимствование произошло внутри create и закончилось окончанием создания (изменяемое заимствование самого себя). Это верно, если созданный объект не содержит ссылку на самого себя. Я думал, что с объектом Mystruct он начал неизменяемое заимствование, которое было бы совместимо со вторым неизменяемым заимствованием Non_mutable_read.

Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
0
1
57
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Из превосходных Распространённых заблуждений о Rust Lifetime:

9) понижение версии mut refs до общих ссылок безопасно

Следствие заблуждения

  • повторное заимствование ссылки завершает ее жизнь и начинает новую

Вы можете передать 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 ref как неизменяемого, не так ли? На удивление нет, поскольку программа ниже не компилируется:

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-ссылку как общую ссылку, вы не получите эту общую ссылку без большой ошибки: это продлевает срок службы mut-ссылки на время повторного заимствования, даже если сама 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-ссылки как общие ссылки, иначе вам будет плохо
  • повторное заимствование мут-ссылки не прекращает ее жизнь, даже если ссылка удалена

Другие вопросы по теме

Как инициализировать две переменные в операторе сопоставления, не вызывая раздражения средства проверки заимствований?
Перенос кода C с изменяемыми заимствованиями — ошибка во время выполнения (пример nappgui)
Не могу заимствовать *self как неизменяемый, но не могу найти обходной путь
Возвращает принадлежащее значение и ссылку на значение
Заимствовать некоторое изменяемое значение дважды, если известно, что изменяемое значение является неизменяемым
Почему мой параметр типа в этом блоке impl не ограничен?
Какую структуру можно создать, чтобы избежать использования RefCell?
Почему средство проверки заимствований в Rust жалуется при использовании итератора, возвращаемого из метода, но не при непосредственном использовании итератора Vec?
Как лучше всего распараллелить код, изменяя несколько фрагментов одного и того же вектора Rust?
Rust: вернуть неизменяемый заем после изменения