Почему я не могу сохранить значение и ссылку на это значение в одной структуре?

224

У меня есть значение, и я хочу сохранить это значение и ссылку на что-то внутри этого значения в моем собственном типе:

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)
}

В каждом из этих случаев я получаю сообщение об ошибке, что одно из значений «не живет достаточно долго». Что означает эта ошибка?

Shepmaster
источник
1
Для последнего примера определение Parentи Childможет помочь ...
Матье М.
1
@MatthieuM. Я обсуждал это, но решил против этого, основываясь на двух взаимосвязанных вопросах. Ни один из этих вопросов не рассматривал определение структуры или рассматриваемый метод, поэтому я подумал, что было бы лучше подражать тому, что людям легче будет сопоставить этот вопрос с их собственной ситуацией. Обратите внимание , что я делать показывать метод подписи в ответ.
Шепмастер

Ответы:

246

Давайте посмотрим на простую реализацию этого :

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добавляет в таблицу, - это обычный способ заявить, что данное значение гарантированно не перемещается.

Смотрите также:

Shepmaster
источник
1
Что-то подобное ( is.gd/wl2IAt ) считается идиоматическим? Т.е. выставлять данные через методы вместо необработанных данных.
Питер Холл
2
@PeterHall конечно, это просто означает, что Combinedвладеет тем, Childкто владеет Parent. Это может иметь или не иметь смысла в зависимости от реальных типов, которые у вас есть. Возвращение ссылок на ваши собственные внутренние данные довольно типично.
Шепмастер
Каково решение проблемы кучи?
Дерекдрееры
@derekdreery Может быть, вы могли бы расширить свой комментарий? Почему весь абзац говорит о корзине owning_ref недостаточно?
Shepmaster
1
@FynnBecker все еще невозможно сохранить ссылку и значение для этой ссылки. Pinэто в основном способ узнать безопасность структуры, содержащей самоссылочный указатель . Возможность использовать необработанный указатель для той же цели существует с Rust 1.0.
Shepmaster
5

Немного другая проблема, которая вызывает очень похожие сообщения компилятора, - это зависимость времени жизни объекта, а не сохранение явной ссылки. Примером этого является библиотека ssh2 . При разработке чего-то большего, чем тестовый проект, заманчиво попытаться поместить Sessionи Channelполученные из этого сеанса вместе друг с другом в структуру, скрывая детали реализации от пользователя. Однако обратите внимание, что у Channelопределения есть время 'sessжизни в его аннотации типа, а у Sessionнего нет.

Это вызывает похожие ошибки компилятора, связанные с временем жизни.

Один из способов решить эту проблему очень простым способом - объявить Sessionвнешнюю сторону в вызывающей стороне, а затем аннотировать ссылку в структуре со временем жизни, аналогично ответу в этом посте на форуме пользователя Rust, где говорится о той же проблеме при инкапсуляции SFTP. , Это не будет выглядеть элегантно и может не всегда применяться - потому что теперь у вас есть две сущности, а не та, которую вы хотели!

Оказывается, ящик для сдачи в аренду или корзина owning_ref из другого ответа также являются решением этой проблемы. Рассмотрим owning_ref, который имеет специальный объект для этой точной цели: OwningHandle. Чтобы избежать перемещения базового объекта, мы размещаем его в куче, используя a Box, что дает нам следующее возможное решение:

use ssh2::{Channel, Error, Session};
use std::net::TcpStream;

use owning_ref::OwningHandle;

struct DeviceSSHConnection {
    tcp: TcpStream,
    channel: OwningHandle<Box<Session>, Box<Channel<'static>>>,
}

impl DeviceSSHConnection {
    fn new(targ: &str, c_user: &str, c_pass: &str) -> Self {
        use std::net::TcpStream;
        let mut session = Session::new().unwrap();
        let mut tcp = TcpStream::connect(targ).unwrap();

        session.handshake(&tcp).unwrap();
        session.set_timeout(5000);
        session.userauth_password(c_user, c_pass).unwrap();

        let mut sess = Box::new(session);
        let mut oref = OwningHandle::new_with_fn(
            sess,
            unsafe { |x| Box::new((*x).channel_session().unwrap()) },
        );

        oref.shell().unwrap();
        let ret = DeviceSSHConnection {
            tcp: tcp,
            channel: oref,
        };
        ret
    }
}

Результатом этого кода является то, что мы больше не можем использовать Session, но он хранится вместе с тем, Channelкоторый мы будем использовать. Поскольку OwningHandleобъект ссылается на то Box, на который ссылается Channel, при хранении его в структуре, мы называем его таковым. ПРИМЕЧАНИЕ: это только мое понимание. У меня есть подозрение, что это может быть неправильно, так как это кажется довольно близко к обсуждению OwningHandleбезопасности .

Одна любопытная деталь здесь является то , что Sessionлогически имеет аналогичные отношения с , TcpStreamкак Channelдолжно Session, но его собственность не принимается и нет типа аннотаций вокруг этого. Вместо этого пользователь должен позаботиться об этом, поскольку документация метода рукопожатия гласит:

Этот сеанс не вступает во владение предоставленным сокетом, рекомендуется, чтобы сокет сохранял время жизни этого сеанса, чтобы гарантировать, что связь выполнена правильно.

Также настоятельно рекомендуется, чтобы предоставленный поток не использовался одновременно где-либо еще в течение этого сеанса, поскольку это может помешать протоколу.

Таким образом, с TcpStreamиспользованием полностью зависит от программиста, чтобы обеспечить правильность кода. При этом OwningHandleвнимание к тому, где происходит «опасная магия», привлекается с помощью unsafe {}блока.

Дальнейшее и более высокоуровневое обсуждение этой проблемы находится в этой ветке форума пользователей Rust - которая включает другой пример и его решение с использованием арендуемого ящика, который не содержит небезопасных блоков.

Andrew Y
источник