Время жизни в Rust (Часть 2)
Введение
В первой части мы рассмотрели простой пример работы времени жизни в Rust. В этой части мы рассмотрим пример с более сложным использованием времени жизни. Попробуем в экземпляре структуры хранить ссылку на экземпляр другой структуры.
Постановка задачи
Мы создадим структуру Customer
, описывающую покупателя, который должен владеть
экземпляром структуры Car
. Покупатель будет иметь возможность покупать,
продавать и обмениваться с другими покупателями автомобилями.
Пишем код
Тип Car
будет лишь включать в себя название модели:
1 2 3 | struct Car { model: String } |
При описании структуры Customer
возникает вопрос о хранении в себе экземпляра
структуры Car
. Для начала попробуем сделать так:
1 2 3 | struct Customer { car: Option<Car> } |
Этот вариант самый простой. Но здесь есть большая проблема. Автомобиль не
является частью клиента. Покупка и продажа будет создавать копии автомобилей,
что приводит к большему увеличению памяти и затраты процессора на копирование.
Хотелось бы хранить не экземпляр структуры Car
, а ссылку на экземпляр. Вот тут
всё становится гораздо сложнее.
Ссылка в структуре
В Rust ссылка должна всегда указывать на выделенный участок памяти, чтобы
избежать проблем. Это означает, что время жизни экземпляра структуры должно быть
не больше чем у ссылки, указывающей на переменную внутри этого экземпляра. Это
означает, что ссылка, хранящаяся внутри структуры Customer
, требует явного
указания параметров времени жизни. Добавим параметры времени жизни:
1 2 3 | struct Customer<'a> { car: Option<&'a Car> } |
Здесь в Customer<'a>
мы задаём имя a
для времени жизни.
В Option<&'a Car>
указываем, что экземпляр структуры Car
имеет время
жизни a
.
Теперь компилятор будет знать, что у экземпляра структуры Customer
время жизни
должно быть таким же как и у экземпляра структуры Car
или меньше. В противном
случае мы будем ссылаться на освобождённый участок памяти.
Методы
После того, как мы объявили структуры, можем приступить к написанию методов для
структуры Customer
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | impl <'a> Customer<'a> { fn new() -> Customer<'a> { Customer { car: None } } fn buy_car(&mut self, c: &'a Car) { self.car = Some(c); } fn sell_car(&mut self) { self.car = None; } } |
В этом коде мы реализовали покупку и продажу автомобиля. Обмен между покупателями мы напишем чуть позже в этой статье.
Обратите внимание на объявление impl <'a> Customer<'a>
. Как вы могли заметить,
объявления параметров времени жизни очень похоже на обобщённое
программирование.
В этом объявлении после ключевого слова impl
задаём имена для параметров
времён жизни. Их может быть несколько. В нашем примере используется один
параметр времени жизни a
.
Использование
Отлично! У нас есть две структуры Customer
и Car
. Давайте попробуем
использовать эти структуры в программе:
1 2 3 4 5 6 7 | fn main() { let car = Car{ model: "Skoda Fabia".to_string() }; let mut bob = Customer::new(); // Боб покупает машину bob.buy_car(&car); } |
Этот код рабочий и компилируется. Но если мы поменяем две строчки местами, то код перестанет компилироваться:
1 2 3 4 5 6 | fn main() { let mut bob = Customer::new(); let car = Car{ model: "Skoda Fabia".to_string() }; bob.buy_car(&car); // Ошибка! } |
Причина ошибки в том, что время жизни у bob
(экземпляр структуры Customer
)
больше чем у car
(экземпляр структуры Car
). В Rust выделение и освобождение
объектов происходит в порядке, обратном порядку объявления. Таким образом, на
непродолжительное время bob
будет хранить в себе ссылку на уже освобождённый
car
. Rust не может нам такое позволить.
Система времени жизни в Rust не может знать, когда ссылка будет не нужна. Давайте посмотрим на ещё один пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 | fn main() { let logan = Car{ model: "Renault Logan".to_string() }; let mut bob = Customer::new(); { // Внутренняя область видимости let fabia = Car{ model: "Skoda Fabia".to_string() }; bob.buy_car(&fabia); // ОШИБКА! bob.buy_car(&logan); } } |
Этот код не компилируется, т. к. время жизни ссылки fabia
меньше, чем у bob
.
С точки зрения разработчика код безопасен, т. к. после внутренней области
видимости bob
больше не ссылается на fabia
. Компилятор строго следует нашему
объявлению метода buy_car(&mut self, c: &'a Car)
, где мы указали, что
экземпляр структуры Car
имеет время жизни a
, и экземпляр Customer
не может
иметь время жизни больше, чем у экземпляра Car
.
Торговля между покупателями
Торговля в нашей задаче подразумевает обмен машинами между экземплярами структур
Customer
. Добавим метод для Customer
:
1 2 3 4 5 6 | fn trade_with(&mut self, other: &mut Customer<'a>) { let tmp = other.car; other.car = self.car; self.car = tmp; } |
Здесь нет ничего особенного. Хочется лишь отметить, что параметр времени жизни
является частью типа данных. Переменная other
имеет тип данных &mut
Customer<'a>
.
Теперь попробуем воспользоваться этим методом:
1 2 3 4 5 6 7 8 9 10 11 12 | fn main() { let fabia = Car{ model: "Skoda Fabia".to_string(); }; let logan = Car{ model: "Renault Logan".to_string(); }; let mut bob = Customer::new(); let mut alice = Customer::new(); bob.buy_car(&fabia); alice.buy_car(&logan); bob.trade_with(&mut alice); } |
Экземпляры структуры Car
требуется создавать перед созданием экземпляров
структур Customer
. Но не имеет значение очерёдность объявлений bob
и
alice
.
Применение правил заимствования
Когда экземпляр хранит в себе ссылку на другой экземпляр, то он заимствует ссылку и применяются правила заимствования (мы перечисляли их в первой части статьи).
Можно позаимствовать множество неизменяемых ссылок на экземпляр, если не создаются ссылки с правом изменения.
1 2 3 4 5 6 7 8 9 | fn main() { let fabia = Car{ model: "Skoda Fabia".to_string() }; let mut bob = Customer::new(); bob.buy_car(&fabia); // bob заимствует неизменяемую ссылку на fabia let p1 = &fabia; // Можем заимствовать дополнительные неизменяемые ссылки let p2 = &fabia; } |
Можно иметь лишь одно заимствование изменяемой ссылки на экземпляр при, этом на этот экземпляр не должно быть каких-либо других заимствований.
1 2 3 4 5 6 7 8 | fn main() { let mut fabia = Car{ model: "Skoda Fabia".to_string() }; let mut bob = Customer::new(); bob.buy_car(&fabia); // bob заимствует изменяемую ссылку на fabia let p1 = &mut fabia; // Ошибка! } |
Вы не можете передать владение экземпляром пока существует заимствование на данный экземпляр.
1 2 3 4 5 6 7 8 | fn main() { let mut fabia = Car{ model: "Skoda Fabia".to_string() }; let mut bob = Customer::new(); bob.buy_car(&fabia); // bob заимствует ссылку на fabia let f = fabia; // Ошибка! Нельзя передать владение. } |
Этот код, неожиданно для программиста, не будет компилироваться:
1 2 3 4 5 6 7 8 9 10 | fn main() { let fabia = Car{ model: "Skoda Fabia".to_string() }; let mut logan = Car{ model: "Renault Logan".to_string() }; let mut bob = Customer::new(); bob.buy_car(&fabia); bob.buy_car(&logan); let p1 = &mut fabia; // Ошибка! } |
С точки зрения программиста код является безопасным. bob
больше не нуждается в
заимствовании ссылки на fabia
и заимствует ссылку на logan
, но компилятор не
может догадаться об этом.
Заключение
Отлично! Мы рассмотрели модель хранения ссылок в структурах. Плохо то, что не всегда эта модель работает. Добавим функцию в наш код:
1 2 3 4 5 | fn shop_for_car(c: &mut Customer) { let car = Car{ model: "BMW M4 Coupe".to_string() }; c.buy_car(&car); // Ошибка! } |
Здесь ошибка заключается в том, что созданный экземпляр структуры Car
будет
освобождён по завершению локальной области видимости функции. Здесь требуется
выделить память в куче (heap) и разместить в ней экземпляр car
. В Rust это
можно сделать с помощью контейнера
Box<T>
. Но об этом мы поговорим
в третьей части серии статей о времени жизни в Rust.