У меня есть значение, и я хочу сохранить это значение и ссылку на что-то внутри этого значения в моем собственном типе:
struct Thing {
count: u32,
}
struct Combined<'a>(Thing, &'a u32);
fn make_combined<'a>() -> Combined<'a> {
let thing = Thing { count: 42 };
Combined(thing, &thing.count)
}
Иногда у меня есть значение, и я хочу сохранить это значение и ссылку на это значение в одной структуре:
struct Combined<'a>(Thing, &'a Thing);
fn make_combined<'a>() -> Combined<'a> {
let thing = Thing::new();
Combined(thing, &thing)
}
Иногда я даже не беру ссылку на значение и получаю ту же ошибку:
struct Combined<'a>(Parent, Child<'a>);
fn make_combined<'a>() -> Combined<'a> {
let parent = Parent::new();
let child = parent.child();
Combined(parent, child)
}
В каждом из этих случаев я получаю сообщение об ошибке, что одно из значений «не живет достаточно долго». Что означает эта ошибка?
lifetime
borrow-checker
rust
Shepmaster
источник
источник
Parent
иChild
может помочь ...Ответы:
Давайте посмотрим на простую реализацию этого :
Это не удастся с ошибкой:
Чтобы полностью понять эту ошибку, вы должны подумать о том, как значения представлены в памяти и что происходит, когда вы перемещаете эти значения. Давайте аннотируем
Combined::new
некоторые гипотетические адреса памяти, которые показывают, где находятся значения:Что должно случиться с
child
? Если значение было просто перемещено, какparent
было, то оно будет ссылаться на память, в которой больше не гарантируется наличие действительного значения. Любой другой фрагмент кода может хранить значения по адресу памяти 0x1000. Доступ к этой памяти, предполагая, что это целое число, может привести к сбоям и / или ошибкам безопасности, и является одной из основных категорий ошибок, которые предотвращает Rust.Это именно та проблема, которую мешают жизни . Время жизни - это метаданные, которые позволяют вам и компилятору знать, как долго значение будет действительным в его текущем месте в памяти . Это важное различие, так как это распространенная ошибка новичков в Rust. Время жизни ржавчины не является периодом времени между созданием объекта и его разрушением!
В качестве аналогии, подумайте об этом следующим образом: в течение жизни человек будет находиться в разных местах, каждый из которых имеет свой адрес. Срок службы Rust связан с адресом, по которому вы в настоящее время проживаете , а не с тем, когда вы умрете в будущем (хотя смерть также изменит ваш адрес). Каждый раз, когда вы переезжаете, это актуально, потому что ваш адрес больше не действителен.
Также важно отметить, что время жизни не меняет ваш код; ваш код контролирует времена жизни, ваши жизни не контролируют код. Содержательная поговорка гласит: «жизни описательные, а не предписывающие».
Давайте аннотируем
Combined::new
некоторые номера строк, которые мы будем использовать для выделения времени жизни:Срок службы бетона от
parent
от 1 до 4 включительно (который я представляю как[1,4]
). Конкретное время жизниchild
- это[2,4]
, а конкретное время жизни возвращаемого значения -[4,5]
. Можно иметь конкретные времена жизни, которые начинаются с нуля - которые будут представлять время жизни параметра для функции или чего-то, что существовало за пределами блока.Обратите внимание, что время жизни
child
само по себе есть[2,4]
, но оно относится к значению со временем жизни[1,4]
. Это хорошо, пока ссылающееся значение становится недействительным, прежде чем ссылочное значение делает. Проблема возникает, когда мы пытаемся вернутьсяchild
из блока. Это будет «чрезмерно продлевать» срок службы сверх его естественной длины.Это новое знание должно объяснить первые два примера. Третий требует рассмотрения реализации
Parent::child
. Скорее всего, это будет выглядеть примерно так:При этом используется время жизни, чтобы избежать записи явных общих параметров времени жизни . Это эквивалентно:
В обоих случаях метод говорит, что
Child
будет возвращена структура, параметризованная с конкретным временем жизниself
. Иными словами,Child
экземпляр содержит ссылку на тотParent
, кто его создал, и поэтому не может жить дольше, чем этотParent
экземпляр.Это также позволяет нам понять, что с нашей функцией создания что-то не так:
Хотя вы, скорее всего, увидите, что это написано в другой форме:
В обоих случаях параметр времени жизни не передается через аргумент. Это означает, что время жизни, которое
Combined
будет параметризовано, ничем не ограничено - оно может быть таким, каким хочет вызывающая сторона. Это бессмысленно, потому что вызывающая сторона может указать время'static
жизни, и нет способа выполнить это условие.Как мне это исправить?
Самое простое и наиболее рекомендуемое решение - не пытаться объединить эти элементы в одну структуру. Делая это, ваша вложенная структура будет имитировать время жизни вашего кода. Поместите типы, которые владеют данными, в структуру вместе, а затем предоставьте методы, которые позволяют вам получать ссылки или объекты, содержащие ссылки по мере необходимости.
Существует особый случай, когда отслеживание времени жизни чрезмерно усердно: когда у вас есть что-то в куче. Это происходит, когда вы используете
Box<T>
, например. В этом случае перемещаемая структура содержит указатель в кучу. Указанное значение останется стабильным, но адрес самого указателя будет перемещен. На практике это не имеет значения, так как вы всегда следуете указателю.Аренда клеть (БОЛЬШЕ НЕ ПОДДЕРЖИВАЮТСЯ ИЛИ ПОДДЕРЖКА) или owning_ref обрешетка способы представления этого дела, но они требуют , чтобы базовый адрес никогда не двигаться . Это исключает мутирующие векторы, которые могут вызвать перераспределение и перемещение выделенных в куче значений.
Примеры проблем, решаемых с помощью проката:
В других случаях вы можете перейти к некоторому типу подсчета ссылок, например, с помощью
Rc
илиArc
.Больше информации
Хотя это теоретически возможно сделать, это приведет к большим сложностям и накладным расходам. Каждый раз, когда объект перемещается, компилятору нужно будет вставить код, чтобы «исправить» ссылку. Это означало бы, что копирование структуры больше не является очень дешевой операцией, которая просто перемещает некоторые биты. Это может даже означать, что подобный код дорогой, в зависимости от того, насколько хорошим будет гипотетический оптимизатор:
Вместо того чтобы заставлять это происходить при каждом шаге, программист сам выбирает, когда это произойдет, создавая методы, которые будут брать соответствующие ссылки только при их вызове.
Тип со ссылкой на себя
Есть один конкретный случай, когда вы можете создать тип со ссылкой на себя. Вы должны использовать что-то вроде,
Option
чтобы сделать это в два этапа:Это делает работу, в каком - то смысле, но созданное значение весьма ограничено - оно не может и не быть перемещены. В частности, это означает, что он не может быть возвращен из функции или передан по значению чему-либо. Функция конструктора показывает ту же проблему с временем жизни, что и выше:
Как насчет
Pin
?Pin
, стабилизированный в Rust 1.33, имеет это в документации модуля :Важно отметить, что «самоссылка» не обязательно означает использование ссылки . Действительно, пример самореферентной структуры конкретно говорит (выделение мое):
Возможность использовать необработанный указатель для этого поведения существует с Rust 1.0. Действительно, владение реф и аренда используют сырые указатели под капотом.
Единственное, что
Pin
добавляет в таблицу, - это обычный способ заявить, что данное значение гарантированно не перемещается.Смотрите также:
источник
Combined
владеет тем,Child
кто владеетParent
. Это может иметь или не иметь смысла в зависимости от реальных типов, которые у вас есть. Возвращение ссылок на ваши собственные внутренние данные довольно типично.Pin
это в основном способ узнать безопасность структуры, содержащей самоссылочный указатель . Возможность использовать необработанный указатель для той же цели существует с Rust 1.0.Немного другая проблема, которая вызывает очень похожие сообщения компилятора, - это зависимость времени жизни объекта, а не сохранение явной ссылки. Примером этого является библиотека ssh2 . При разработке чего-то большего, чем тестовый проект, заманчиво попытаться поместить
Session
иChannel
полученные из этого сеанса вместе друг с другом в структуру, скрывая детали реализации от пользователя. Однако обратите внимание, что уChannel
определения есть время'sess
жизни в его аннотации типа, а уSession
него нет.Это вызывает похожие ошибки компилятора, связанные с временем жизни.
Один из способов решить эту проблему очень простым способом - объявить
Session
внешнюю сторону в вызывающей стороне, а затем аннотировать ссылку в структуре со временем жизни, аналогично ответу в этом посте на форуме пользователя Rust, где говорится о той же проблеме при инкапсуляции SFTP. , Это не будет выглядеть элегантно и может не всегда применяться - потому что теперь у вас есть две сущности, а не та, которую вы хотели!Оказывается, ящик для сдачи в аренду или корзина owning_ref из другого ответа также являются решением этой проблемы. Рассмотрим owning_ref, который имеет специальный объект для этой точной цели:
OwningHandle
. Чтобы избежать перемещения базового объекта, мы размещаем его в куче, используя aBox
, что дает нам следующее возможное решение:Результатом этого кода является то, что мы больше не можем использовать
Session
, но он хранится вместе с тем,Channel
который мы будем использовать. ПосколькуOwningHandle
объект ссылается на тоBox
, на который ссылаетсяChannel
, при хранении его в структуре, мы называем его таковым. ПРИМЕЧАНИЕ: это только мое понимание. У меня есть подозрение, что это может быть неправильно, так как это кажется довольно близко к обсуждениюOwningHandle
безопасности .Одна любопытная деталь здесь является то , что
Session
логически имеет аналогичные отношения с ,TcpStream
какChannel
должноSession
, но его собственность не принимается и нет типа аннотаций вокруг этого. Вместо этого пользователь должен позаботиться об этом, поскольку документация метода рукопожатия гласит:Таким образом, с
TcpStream
использованием полностью зависит от программиста, чтобы обеспечить правильность кода. При этомOwningHandle
внимание к тому, где происходит «опасная магия», привлекается с помощьюunsafe {}
блока.Дальнейшее и более высокоуровневое обсуждение этой проблемы находится в этой ветке форума пользователей Rust - которая включает другой пример и его решение с использованием арендуемого ящика, который не содержит небезопасных блоков.
источник