Ответ 1
Давайте посмотрим на простую реализацию этого:
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 являются способами представления этого случая, но они требуют, чтобы базовый адрес никогда не перемещался. Это исключает мутирующие векторы, которые могут вызвать перераспределение и перемещение выделенных в куче значений.
Примеры проблем, решаемых с помощью проката:
- Есть ли собственная версия String :: chars?
- Возврат RWLockReadGuard независимо от метода
- Как я могу вернуть итератор для заблокированного члена структуры в Rust?
- Как вернуть ссылку на подзначение значения, находящегося под мьютексом?
- Как сохранить результат, используя десериализацию Serde Zero-copy Hyper Chunk с поддержкой Futures?
- Как сохранить ссылку, не имея дело с жизнями?
В других случаях вы можете перейти к некоторому типу подсчета ссылок, например, используя 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
добавляет в таблицу, это обычный способ заявить, что данное значение гарантированно не будет перемещаться.
Смотрите также: