Давайте посмотрим на простую реализацию этого :
struct Parent {
count: u32,
}
struct Child<'a> {
parent: &'a Parent,
}
struct Combined<'a> {
parent: Parent,
child: Child<'a>,
}
impl<'a> Combined<'a> {
fn new() -> Self {
let parent = Parent { count: 42 };
let child = Child { parent: &parent };
Combined { parent, child }
}
}
fn main() {}
Это не удастся с ошибкой:
error[E0515]: cannot return value referencing local variable `parent`
--> src/main.rs:19:9
|
17 | let child = Child { parent: &parent };
| ------- `parent` is borrowed here
18 |
19 | Combined { parent, child }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function
error[E0505]: cannot move out of `parent` because it is borrowed
--> src/main.rs:19:20
|
14 | impl<'a> Combined<'a> {
| -- lifetime `'a` defined here
...
17 | let child = Child { parent: &parent };
| ------- borrow of `parent` occurs here
18 |
19 | Combined { parent, child }
| -----------^^^^^^---------
| | |
| | move out of `parent` occurs here
| returning this value requires that `parent` is borrowed for `'a`
Чтобы полностью понять эту ошибку, вы должны подумать о том, как значения представлены в памяти и что происходит, когда вы перемещаете
эти значения. Давайте аннотируем Combined::new
некоторые гипотетические адреса памяти, которые показывают, где находятся значения:
let parent = Parent { count: 42 };
// `parent` lives at address 0x1000 and takes up 4 bytes
// The value of `parent` is 42
let child = Child { parent: &parent };
// `child` lives at address 0x1010 and takes up 4 bytes
// The value of `child` is 0x1000
Combined { parent, child }
// The return value lives at address 0x2000 and takes up 8 bytes
// `parent` is moved to 0x2000
// `child` is ... ?
Что должно случиться с child
? Если значение было просто перемещено, как parent
было, то оно будет ссылаться на память, в которой больше не гарантируется наличие действительного значения. Любой другой фрагмент кода может хранить значения по адресу памяти 0x1000. Доступ к этой памяти, предполагая, что это целое число, может привести к сбоям и / или ошибкам безопасности, и является одной из основных категорий ошибок, которые предотвращает Rust.
Это именно та проблема, которую мешают жизни . Время жизни - это метаданные, которые позволяют вам и компилятору знать, как долго значение будет действительным в его текущем месте в памяти . Это важное различие, так как это распространенная ошибка новичков в Rust. Время жизни ржавчины не является периодом времени между созданием объекта и его разрушением!
В качестве аналогии, подумайте об этом следующим образом: в течение жизни человек будет находиться в разных местах, каждый из которых имеет свой адрес. Срок службы Rust связан с адресом, по которому вы в настоящее время проживаете , а не с тем, когда вы умрете в будущем (хотя смерть также изменит ваш адрес). Каждый раз, когда вы переезжаете, это актуально, потому что ваш адрес больше не действителен.
Также важно отметить, что время жизни не меняет ваш код; ваш код контролирует времена жизни, ваши жизни не контролируют код. Содержательная поговорка гласит: «жизни описательные, а не предписывающие».
Давайте аннотируем Combined::new
некоторые номера строк, которые мы будем использовать для выделения времени жизни:
{ // 0
let parent = Parent { count: 42 }; // 1
let child = Child { parent: &parent }; // 2
// 3
Combined { parent, child } // 4
} // 5
Срок службы бетона от parent
от 1 до 4 включительно (который я представляю как [1,4]
). Конкретное время жизни child
- это [2,4]
, а конкретное время жизни возвращаемого значения - [4,5]
. Можно иметь конкретные времена жизни, которые начинаются с нуля - которые будут представлять время жизни параметра для функции или чего-то, что существовало за пределами блока.
Обратите внимание, что время жизни child
само по себе есть [2,4]
, но оно относится к значению со временем жизни [1,4]
. Это хорошо, пока ссылающееся значение становится недействительным, прежде чем ссылочное значение делает. Проблема возникает, когда мы пытаемся вернуться child
из блока. Это будет «чрезмерно продлевать» срок службы сверх его естественной длины.
Это новое знание должно объяснить первые два примера. Третий требует рассмотрения реализации Parent::child
. Скорее всего, это будет выглядеть примерно так:
impl Parent {
fn child(&self) -> Child { /* ... */ }
}
При этом используется время жизни, чтобы избежать записи явных общих параметров времени жизни . Это эквивалентно:
impl Parent {
fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}
В обоих случаях метод говорит, что Child
будет возвращена структура, параметризованная с конкретным временем жизни
self
. Иными словами, Child
экземпляр содержит ссылку на тот Parent
, кто его создал, и поэтому не может жить дольше, чем этот
Parent
экземпляр.
Это также позволяет нам понять, что с нашей функцией создания что-то не так:
fn make_combined<'a>() -> Combined<'a> { /* ... */ }
Хотя вы, скорее всего, увидите, что это написано в другой форме:
impl<'a> Combined<'a> {
fn new() -> Combined<'a> { /* ... */ }
}
В обоих случаях параметр времени жизни не передается через аргумент. Это означает, что время жизни, которое Combined
будет параметризовано, ничем не ограничено - оно может быть таким, каким хочет вызывающая сторона. Это бессмысленно, потому что вызывающая сторона может указать время 'static
жизни, и нет способа выполнить это условие.
Как мне это исправить?
Самое простое и наиболее рекомендуемое решение - не пытаться объединить эти элементы в одну структуру. Делая это, ваша вложенная структура будет имитировать время жизни вашего кода. Поместите типы, которые владеют данными, в структуру вместе, а затем предоставьте методы, которые позволяют вам получать ссылки или объекты, содержащие ссылки по мере необходимости.
Существует особый случай, когда отслеживание времени жизни чрезмерно усердно: когда у вас есть что-то в куче. Это происходит, когда вы используете
Box<T>
, например. В этом случае перемещаемая структура содержит указатель в кучу. Указанное значение останется стабильным, но адрес самого указателя будет перемещен. На практике это не имеет значения, так как вы всегда следуете указателю.
Аренда клеть (БОЛЬШЕ НЕ ПОДДЕРЖИВАЮТСЯ ИЛИ ПОДДЕРЖКА) или owning_ref обрешетка способы представления этого дела, но они требуют , чтобы базовый адрес никогда не двигаться . Это исключает мутирующие векторы, которые могут вызвать перераспределение и перемещение выделенных в куче значений.
Примеры проблем, решаемых с помощью проката:
В других случаях вы можете перейти к некоторому типу подсчета ссылок, например, с помощью Rc
или Arc
.
Больше информации
После перехода parent
в структуру, почему компилятор не может получить новую ссылку parent
и назначить ее child
в структуре?
Хотя это теоретически возможно сделать, это приведет к большим сложностям и накладным расходам. Каждый раз, когда объект перемещается, компилятору нужно будет вставить код, чтобы «исправить» ссылку. Это означало бы, что копирование структуры больше не является очень дешевой операцией, которая просто перемещает некоторые биты. Это может даже означать, что подобный код дорогой, в зависимости от того, насколько хорошим будет гипотетический оптимизатор:
let a = Object::new();
let b = a;
let c = b;
Вместо того чтобы заставлять это происходить при каждом шаге, программист сам выбирает, когда это произойдет, создавая методы, которые будут брать соответствующие ссылки только при их вызове.
Тип со ссылкой на себя
Есть один конкретный случай, когда вы можете создать тип со ссылкой на себя. Вы должны использовать что-то вроде, Option
чтобы сделать это в два этапа:
#[derive(Debug)]
struct WhatAboutThis<'a> {
name: String,
nickname: Option<&'a str>,
}
fn main() {
let mut tricky = WhatAboutThis {
name: "Annabelle".to_string(),
nickname: None,
};
tricky.nickname = Some(&tricky.name[..4]);
println!("{:?}", tricky);
}
Это делает работу, в каком - то смысле, но созданное значение весьма ограничено - оно не может и не быть перемещены. В частности, это означает, что он не может быть возвращен из функции или передан по значению чему-либо. Функция конструктора показывает ту же проблему с временем жизни, что и выше:
fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }
Как насчет Pin
?
Pin
, стабилизированный в Rust 1.33, имеет это в документации модуля :
Ярким примером такого сценария было бы создание самоссылочных структур, поскольку перемещение объекта с указателями на себя сделает их недействительными, что может привести к неопределенному поведению.
Важно отметить, что «самоссылка» не обязательно означает использование ссылки . Действительно, пример самореферентной структуры конкретно говорит (выделение мое):
Мы не можем сообщить об этом компилятору с помощью обычной ссылки, поскольку этот шаблон нельзя описать с помощью обычных правил заимствования. Вместо этого мы используем необработанный указатель , хотя он, как известно, не равен нулю, поскольку мы знаем, что он указывает на строку.
Возможность использовать необработанный указатель для этого поведения существует с Rust 1.0. Действительно, владение реф и аренда используют сырые указатели под капотом.
Единственное, что Pin
добавляет в таблицу, - это обычный способ заявить, что данное значение гарантированно не перемещается.
Смотрите также:
Parent
иChild
может помочь ...