- PVSM.RU - https://www.pvsm.ru -
Один из наиболее частых вопросов у новичков «Как мне угодить проверке заимствования?». Проверка заимствования — одна из крутых частей в кривой обучения Rust и, понятно, что у новичков возникают трудности применения этой концепции в своих программах.
Только недавно на сабредите Rust [1] появился вопрос «Советы как не воевать с проверкой заимствования?» [2].
Многие члены Rust-сообщества привели полезные советы как избежать неприятностей связанных с этой проверкой, советы, которые проливают свет на то, как вы должны проектировать свой код на Rust (подсказка: не так, как вы это делаете на Java).
В этом посте я постараюсь показать несколько псевдо-реальных примеров распространенных ловушек.
Для начала, резюмирую правила проверки заимствования:
Т.к. у вас может быть только одно изменяемое заимствование единовременно, то вы можете получить проблемы, если захотите изменить что-то дважды в одной функции. Даже, если заимствования не пересекаются, проверка заимствования пожалуется.
Посмотрите на пример, который не компилируется.
struct Person {
name: String,
age: u8,
}
impl Person {
fn new(name: &str, age: u8) -> Person {
Person {
name: name.into(),
age: age,
}
}
fn celebrate_birthday(&mut self) {
self.age += 1;
println!("{} is now {} years old!", self.name, self.age);
}
fn name(&self) -> &str {
&self.name
}
}
fn main() {
let mut jill = Person::new("Jill", 19);
let jill_ref_mut = &mut jill;
jill_ref_mut.celebrate_birthday();
println!("{}", jill.name()); // невозможно заимствовать jill как неизменяемое
// потому что это уже заимствовано как
// изменяемое
}
Проблема здесь в том, что у нас есть изменяемое заимствование jill и затем мы опять пытаемся его использовать для печати имени. Исправить ситуацию поможет ограничение области видимости заимствования.
fn main() {
let mut jill = Person::new("Jill", 19);
{
let jill_ref_mut = &mut jill;
jill_ref_mut.celebrate_birthday();
}
println!("{}", jill.name());
}
В целом, это хорошая идея ограничивать область видимости своих изменяемых ссылок. Это позволяет избегать проблем похожих на ту, которая продемонстрирована выше.
Вы часто хотите сцепить вызовы функций для уменьшения количества локальных переменных и let-присвоений. Представьте, что у вас есть библиотека, которая предоставляет в пользование структуры Person и Name. Вы хотите получть изменяемую ссылку на имя человека и обновить его.
#[derive(Clone)]
struct Name {
first: String,
last: String,
}
impl Name {
fn new(first: &str, last: &str) -> Name {
Name {
first: first.into(),
last: last.into(),
}
}
fn first_name(&self) -> &str {
&self.first
}
}
struct Person {
name: Name,
age: u8,
}
impl Person {
fn new(name: Name, age: u8) -> Person {
Person {
name: name,
age: age,
}
}
fn name(&self) -> Name {
self.name.clone()
}
}
fn main() {
let name = Name::new("Jill", "Johnson");
let mut jill = Person::new(name, 20);
let name = jill.name().first_name(); // заимствованное значение
// не живёт достаточно долго
}
Проблема здесь в том, что Person::name возвращает владение переменной вместо ссылки на неё. Если мы пытаемся получить ссылку используя Name::first_name, то проверка заимствования пожалуется. Как только блок завершится, значение возвращённое из jill.name() будет удалено и name окажется висячим указателем.
Решение — ввести временную переменную.
fn main() {
let name = Name::new("Jill", "Johnson");
let mut jill = Person::new(name, 20);
let name = jill.name();
let name = name.first_name();
}
По-хорошему, мы должны вернуть &Name из Person::name, но есть несколько случаев в которых возврат владения занчением — единственный разумный варинт. Если это случится, то хорошо бы знать, как исправить свой код.
Иногда вы сталкиваетесь с циклическими ссылками в своём коде. Это то, что я слишком часто использовал, программируя на Си. Борьба с проверкой заимствования в Rust показала мне насколько опасным может быть такой код.
Создадим представление занятий и записанных на них учеников. Занятие ссылается на учеников, а ученики в свою очередь сохраняют ссылки на занятия, которые они посещают.
struct Person<'a> {
name: String,
classes: Vec<&'a Class<'a>>,
}
impl<'a> Person<'a> {
fn new(name: &str) -> Person<'a> {
Person {
name: name.into(),
classes: Vec::new(),
}
}
}
struct Class<'a> {
pupils: Vec<&'a Person<'a>>,
teacher: &'a Person<'a>,
}
impl<'a> Class<'a> {
fn new(teacher: &'a Person<'a>) -> Class<'a> {
Class {
pupils: Vec::new(),
teacher: teacher,
}
}
fn add_pupil(&'a mut self, pupil: &'a mut Person<'a>) {
pupil.classes.push(self);
self.pupils.push(pupil);
}
}
fn main() {
let jack = Person::new("Jack");
let jill = Person::new("Jill");
let teacher = Person::new("John");
let mut borrow_chk_class = Class::new(&teacher);
borrow_chk_class.add_pupil(&mut jack);
borrow_chk_class.add_pupil(&mut jill);
}
Если мы попытаемся скомпилировать код, то подвергнемся бомбардировке сообщений об ошибках. Основная проблема в том, что мы пытаемся сохранить ссылки на занятия у учеников и наборот. Когда переменные будут удаляться (в обратном созданию порядке), teacher также удалится, но jill и jack всё так же будут ссылаться на занятие, котрое должно быть удалено.
Простейшее (но сложночитаемое) решение — избежать проверки заимствования и использовать Rc.
use std::rc::Rc;
use std::cell::RefCell;
struct Person {
name: String,
classes: Vec<Rc<RefCell<Class>>>,
}
impl Person {
fn new(name: &str) -> Person {
Person {
name: name.into(),
classes: Vec::new(),
}
}
}
struct Class {
pupils: Vec<Rc<RefCell<Person>>>,
teacher: Rc<RefCell<Person>>,
}
impl Class {
fn new(teacher: Rc<RefCell<Person>>) -> Class {
Class {
pupils: Vec::new(),
teacher: teacher.clone(),
}
}
fn pupils_mut(&mut self) -> &mut Vec<Rc<RefCell<Person>>> {
&mut self.pupils
}
fn add_pupil(class: Rc<RefCell<Class>>, pupil: Rc<RefCell<Person>>) {
pupil.borrow_mut().classes.push(class.clone());
class.borrow_mut().pupils_mut().push(pupil);
}
}
fn main() {
let jack = Rc::new(RefCell::new(Person::new("Jack")));
let jill = Rc::new(RefCell::new(Person::new("Jill")));
let teacher = Rc::new(RefCell::new(Person::new("John")));
let mut borrow_chk_class = Rc::new(RefCell::new(Class::new(teacher)));
Class::add_pupil(borrow_chk_class.clone(), jack);
Class::add_pupil(borrow_chk_class, jill);
}
Отметьте, что теперь у нас нет гарантий безопасности, которая даёт проверка заимствования.
Как указал /u/steveklabnik1, цитата [3]:
Отметьте, что Rc и RefCell оба полагаются на механизм обеспечения безопасности во времени выполнения, т.е. мы теряем проверки времени компиляции: для примера, RefCell запаникует, в случае если мы попытаемся вызвать borrow_mut дважды.
Возможно, лучшим вариантом будет реорганизовать код таким образом, чтобы циклические ссылки не потребовались.
Если вы когда либо нормализовывали отношения в базе данных, то это похожий случай. Мы сохраним ссылки между учеником и занятием в отдельной структуре.
struct Enrollment<'a> {
person: &'a Person,
class: &'a Class<'a>,
}
impl<'a> Enrollment<'a> {
fn new(person: &'a Person, class: &'a Class<'a>) -> Enrollment<'a> {
Enrollment {
person: person,
class: class,
}
}
}
struct Person {
name: String,
}
impl Person {
fn new(name: &str) -> Person {
Person {
name: name.into(),
}
}
}
struct Class<'a> {
teacher: &'a Person,
}
impl<'a> Class<'a> {
fn new(teacher: &'a Person) -> Class<'a> {
Class {
teacher: teacher,
}
}
}
struct School<'a> {
enrollments: Vec<Enrollment<'a>>,
}
impl<'a> School<'a> {
fn new() -> School<'a> {
School {
enrollments: Vec::new(),
}
}
fn enroll(&mut self, pupil: &'a Person, class: &'a Class) {
self.enrollments.push(Enrollment::new(pupil, class));
}
}
fn main() {
let jack = Person::new("Jack");
let jill = Person::new("Jill");
let teacher = Person::new("John");
let borrow_chk_class = Class::new(&teacher);
let mut school = School::new();
school.enroll(&jack, &borrow_chk_class);
school.enroll(&jill, &borrow_chk_class);
}
В любом случае, такой подход лучше. Нет никаких оснований к тому, чтобы ученик хранил информацию о том, какие занятия он посещает и в самом занятии не должна хранится информаци о том, кто его посещает. Если эта информация понадобится, то она может быть получена из списка посещений.
Если вы так и не поняли, почему правила проверки заимствования являются такими, какие они есть, то это объясненние пользователя реддита /u/Fylwind может помочь. Он замечательно привёл аналогию с блокировкой на чтение-запись [4]:
Проверку заимствования я представляю себе как систему блокировки (блокировка на чтение-запись). Если у вас есть неизменяемая ссылка, то она представляется как разделяемая блокировка на объект, в случае, если у вас изменяемая ссылка, то это уже вроде эксклюзивной блокировки. И, как в любой системе блокировок, удержание блокировки дольше чем требуется — плохая идея. Особенно это плохо для изменяемых ссылок.
В конечном итоге, если на первый взгляд вам кажется, что вы боретесь с проверкой заимствования, то вы полюбите её, как только научитесь ей пользоваться.
Автор: scalavod
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/rust/234710
Ссылки в тексте:
[1] Rust: https://www.reddit.com/r/rust/
[2] «Советы как не воевать с проверкой заимствования?»: https://www.reddit.com/r/rust/comments/5ny09j/tips_to_not_fight_the_borrow_checker/
[3] цитата: https://www.reddit.com/r/rust/comments/5obein/fighting_the_borrow_checker/dci15nw/
[4] привёл аналогию с блокировкой на чтение-запись: https://www.reddit.com/r/rust/comments/5ny09j/tips_to_not_fight_the_borrow_checker/dcf9zdv/
[5] Источник: https://habrahabr.ru/post/319808/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.