Боротьба з перевіркою запозичення

Один з найбільш частих питань у новачків «Як мені догодити перевірці запозичення?». Перевірка запозичення — одна з крутих частин кривий навчання 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 може допомогти. Він чудово навів аналогію з блокуванням на читання-запис:
Перевірку запозичення я уявляю собі як систему блокування (блокування на читання-запис). Якщо у вас є незмінна посилання, то вона представляється як спільна блокування на об'єкт, у випадку, якщо у вас змінювана посилання, то це вже ніби ексклюзивної блокування. І, як у будь-якій системі блокувань, утримання блокування довше ніж вимагається — погана ідея. Особливо це погано для змінюваних посилань.
зрештою, якщо на перший погляд вам здається, що ви боретеся з перевіркою запозичення, то ви полюбите її, як тільки навчитеся їй користуватися.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.