Один из наиболее частых вопросов у новичков «Как мне угодить проверке заимствования?». Проверка заимствования — одна из крутых частей в кривой обучения 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<RefCell>.


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


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

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

Поделиться с друзьями
-->

Комментарии (16)


  1. aamonster
    18.01.2017 08:55

    Можно подробнее про последний пример и "нормализацию"?
    Что я вижу — она создаёт проблему по быстродействию. Как только вам понадобится список занятий для ученика — придётся перебирать общий список enrollments. В БД для того, чтобы делать это быстро — есть ключи и другие ухищрения, да и требования по быстродействию обычно не столь суровы, как к структурам данных в памяти, но всё равно порой разработчики сознательно идут на денормализацию базы.


    1. DarkEld3r
      18.01.2017 13:51
      +2

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


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


      1. aamonster
        18.01.2017 15:53

        «Организовать с ключами» — понятное дело, если там сколько-нибудь заметное количество элементов — мало-мальски опытный программист туда воткнёт map. Но это совсем не бесплатно (казалось бы, Rust нужен, чтобы бесплатно или задёшево давать гарантии «ссылочной целостности» и т.п.)

        Насчёт «во вторых» — ну да, вроде Rc, который упомянут в статье — вроде более-менее классический weak pointer получается (один из традиционных путей решения аналогичной задачи на C++). Но почему синтаксис такой страшный? Даже страшнее, чем в C++. Нельзя ли это переписать по человечески? (нутром чую, что можно)
        Опять же, автор называет это решение «сложночитаемым» (я его понимаю) и рекомендует второе, дорогое — как «более правильное».


        1. DarkEld3r
          19.01.2017 03:17
          +2

          казалось бы, Rust нужен, чтобы бесплатно или задёшево давать гарантии «ссылочной целостности» и т.п.

          Язык не всемогущ. (:


          Сейчас правила "заимствования" в языке довольно просты, пусть и не всегда достаточно гибки. При этом данная часть всё равно считается сложной для освоения. Честно говоря, плохо представляю как это можно было бы улучшить, чтобы и изучение не усложнить и код кучей дополнительных аннотаций не загромождать.


          Опять же, мне кажется, что возникшие неудобства вполне окупаются дополнительными гарантиями. Понимаю, что могут быть и другие мнения — чего уж там, некоторым и динамика по душе.


          Нельзя ли это переписать по человечески? (нутром чую, что можно)

          Речь о вещах типа Vec<Rc<RefCell<Class>>>? Ну можно тайпдефы использовать. А вообще мне такая матрёшка даже нравится: оно гибче, чем заводить отдельные классы на каждую комбинацию возможностей.


  1. erwins22
    18.01.2017 09:30

    вообще тут проблема на мой взгляд в том что нет неразрывных конструкций

    в 3 случае мы имеем 2 взаимосвязанных delete в принципе тут мог решить проблему алгоритм разрешения циклических ссылок аналогичный алгоритму сборки муссора
    для этого delete пришлось бы объединить в атомик (внутри атомика компилятор допускает висящие ссылки, но не допускает их использования) думаю к этому придут

    в 1 случае компилятору ничто не мешает увидеть, что область использования переменной jill_ref_mut заканчивается после строки jill_ref_mut.celebrate_birthday(); тут компилятор тоже может без проблем разобраться однозначно сам.

    аналогично случай 2.


    1. erwins22
      18.01.2017 12:49

      думаю в 1.15 поправят


  1. msts2017
    18.01.2017 10:15

    по логике, у ученика должна быть ссылка на школу учеником которой он является, как тут быть?


    1. BlessMaster
      18.01.2017 10:54
      +1

      «по логике» — достаточно сомнительное обоснование наличия ссылки у ученика.
      Как вариант, индекс или id вполне могут выполнять роль «ссылки». Всё зависит от задачи, а задачи могут со временем меняться.


      1. msts2017
        18.01.2017 11:17

        у меня есть ученик, мне нужно получить школу учеником которой он является, как? понятное дело без каких либо глобальных списков школа — ученик


        1. BlessMaster
          18.01.2017 11:36
          +2

          А куда будет указывать ссылка?
          Ссылку можно сделать, если известно, что объект «ученик» будет жить меньше, чем объект «школа».
          Односторонние ссылки в этом случае проблемы не составляют.
          У индекса/id есть преимущество — он не прибивает объект, на который ссылается, гвоздями к определённому месту в памяти.


          1. msts2017
            18.01.2017 11:59
            +2

            А куда будет указывать ссылка?

            по логике, на полноценный объект школа.

            я в Rustе не разбираюсь, вот и спрашиваю, раз пропагандируется отказ от циклических ссылок значит должна быть вменяемая альтернатива, глобальные (самопальные!!!) списки к этому не относятся.


        1. BlessMaster
          18.01.2017 11:42
          +2

          Вариант, когда нужны взаимные ссылки, предложен в статье — «владеющая ссылка».
          К сожалению в статье не упомянут другой недостаток Rc/Arc — при неправильном употреблении они могут привести к утечке памяти.


          1. msts2017
            18.01.2017 12:12

            Из взаимных ссылок можно выделить специальный вид, назовем — «неотъемлемого владения», например школа-ученик, ученик без школы в принципе существовать не может, когда, гм, персона переходит из одной школы в другую создается новый объект ученик. По логике, стандартизация в языке такого вида ссылок, без костылей типа Rc, закрыло бы львиную долю случаев, как-то так.


            1. BlessMaster
              18.01.2017 13:35
              +4

              Хорошо, в данном случае вполне можно поместить школу владеющую списком учеников в Rc, а ученикам дать Weak ссылку на школу. Это вполне будет работать.

              Но это сразу станет немного «дороже» — часть проверок перенесётся в рантайм, в программе придётся обрабатывать случаи, когда «что-то пошло не так».
              У всего своя цена, серебряных пуль нет.

              Поэтому в первую очередь ставится вопрос: зачем эта обратная ссылка на школу? Нужна ли она? Можно ли обойтись без неё? Какая стоит задача?

              В конце концов, если уж очень нужно, никто же не мешает сделать структуру со школами и учениками на сырых указателях «как в старом добром Си» и работать с нею на свой страх и риск, огребая все щедро рассыпанные на пути грабли. Ссылки и времена жизни сделали ведь не для того, чтобы портить программисту жизнь, — это мощный инструмент автоматического контроля со статическими гарантиями.

              Чуть выше я предложил использовать индексы или id — это не то же самое, что список пар «ученик-школа», упомянутый в статье. Тут уместнее предположить две владеющих структуры списком школ и списком учеников, а школы и ученики могут, если нужно, просто хранить номера или ключи в этой структуре.

              Недостатки ссылок вполне перечислены в статье — ссылка может быть либо одна изменяемая, либо много — на неизменяемый объект. Меньше ссылок, связывающих состояние — меньше ограничений. Преимущество ссылки — более быстрое получение объекта при необходимости и статические гарантии, что объект существует.

              Получение школы или ученика из таблицы по индексу — достаточно дешёвая операция, чтобы рассмотреть её как альтернативу хранению ссылки, при этом, если количество объектов небольшое, то можно сэкономить на размере хранимого значения и что-то выиграть с кешами процессора. Плюс к этому, номер в списке вполне можно применить к разным таблицам, хранящим разную информацию об объекте. Но у массивов и индексов в них есть и существенный недостаток, они плохо применимы к динамическим данным, когда списки активно меняются и элементы массово удаляются — здесь уже нужно решать вопросы с фрагментацией списков или обновлением индексов, что дорого. С другой стороны с указателями — это ещё сложнее, а со ссылками об этом можно забыть. Минус статические гарантии, плюс проверки и риск ошибок.

              Как компромис в такой ситуации можно использовать {Hash,BTree}Map с идентификаторами, учитывая, что получить из него что-то по ключу — это уже более дорогая операция, чем обычный индекс. Так же без гарантий и с лишними проверками и рисками.

              Rust, как системный язык, даёт разные инструменты для тонкого контроля над тем, как работает программа. При этом инструменты далеко не всегда взаимозаменяемые, каждый со своими требованиями, ограничениями, ценой, хотя может показаться «здесь куча разных способов сделать одно и то же». Поэтому ещё раз повторюсь: в первую очередь всё определяет задача, а уже дальше нужно думать, какой инструмент для её решения использовать. Делать что-то «для удобства», «так привычно» или «на всякий случай» — плохая мотивация.


              1. msts2017
                18.01.2017 23:31

                Вы так пишите как будто получение по ученику — школы что-то невероятное, хотя это типовая ситуация, ладно, расширим модель, у нас есть объект (структура в данном случае) — «ГородскаяОлимпиада» со списком объектов «УчастникОлимпиады», очевидно (мне), «олимпиада» должна знать из какой школы ученик, статистику по школам вести, например, да хотя-бы куда поздравления слать, да можно обойтись двумя ссылками (в «УчастникОлимпиады») — ученик и школа, но это несерьезно, мало того что надо постоянно согласовывать (вручную) чтоб ученик действительно был из заданной школы (при этом в совершенно постороннем объекте!), так и в случае если надо расширить набор полей, надо их добавлять не в, или, не только в ученика, а в, опять-же, совершенно посторонний объект, если-же в «УчастникОлимпиады» будет ссылка на ученика а нем на школу, по сути, будет тоже самое, только памяти жрать меньше и управлять правильнее.


  1. Dageron
    18.01.2017 13:23
    +1

    Если честно, прочитав изначально заголовок статьи, подумал, что речь в ней пойдет об обманках систем вроде «антиплагиат», которые «проверяют заимствования» в научных работах и других источниках.

    Вместо этого узнал интересные особенности незнакомого для себя языка программирования Rust.
    Бывает же такое!