Борьба с проверкой заимствования

в 23:45, , рубрики: borrow checker, Rust, проверка заимствования, метки: ,

Один из наиболее частых вопросов у новичков «Как мне угодить проверке заимствования?». Проверка заимствования — одна из крутых частей в кривой обучения Rust и, понятно, что у новичков возникают трудности применения этой концепции в своих программах.

Только недавно на сабредите Rust появился вопрос «Советы как не воевать с проверкой заимствования?».

Многие члены Rust-сообщества привели полезные советы как избежать неприятностей связанных с этой проверкой, советы, которые проливают свет на то, как вы должны проектировать свой код на Rust (подсказка: не так, как вы это делаете на Java).

В этом посте я постараюсь показать несколько псевдо-реальных примеров распространенных ловушек.

Для начала, резюмирую правила проверки заимствования:

  1. У вас единовременно может быть только одна изменяемая ссылка на переменную
  2. У вас может быть столько незменяемых ссылок на переменную, сколько потребуется
  3. Вы не можете смешивать изменяемые и неизменяемые ссылки на одну переменную

Долгоживущие блоки

Т.к. у вас может быть только одно изменяемое заимствование единовременно, то вы можете получить проблемы, если захотите изменить что-то дважды в одной функции. Даже, если заимствования не пересекаются, проверка заимствования пожалуется.

Посмотрите на пример, который не компилируется.

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, цитата:

Отметьте, что 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 может помочь. Он замечательно привёл аналогию с блокировкой на чтение-запись:

Проверку заимствования я представляю себе как систему блокировки (блокировка на чтение-запись). Если у вас есть неизменяемая ссылка, то она представляется как разделяемая блокировка на объект, в случае, если у вас изменяемая ссылка, то это уже вроде эксклюзивной блокировки. И, как в любой системе блокировок, удержание блокировки дольше чем требуется — плохая идея. Особенно это плохо для изменяемых ссылок.

В конечном итоге, если на первый взгляд вам кажется, что вы боретесь с проверкой заимствования, то вы полюбите её, как только научитесь ей пользоваться.

Автор: scalavod

Источник

Поделиться

* - обязательные к заполнению поля