В следующем коде я был удивлен жалобой на заимствование
#[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.
Из превосходных Распространённых заблуждений о 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-ссылки как общие ссылки, иначе вам будет плохо
- повторное заимствование мут-ссылки не прекращает ее жизнь, даже если ссылка удалена