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


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

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

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

2 129

2 ответа:

давайте посмотрим на простая реализация этой:

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 p = Parent { count: 42 };
        let c = Child { parent: &p };

        Combined { parent: p, child: c }
    }
}

fn main() {}

это не удастся с немного очищенной ошибкой:

error: `p` does not live long enough
  --> src/main.rs:17:34
   |
17 |         let c = Child { parent: &p };
   |                                  ^
   |
note: reference must be valid for the lifetime 'a as defined
      on the block at 15:21...
  --> src/main.rs:15:22
   |
15 |     fn new() -> Self {
   |                      ^
note: ...but borrowed value is only valid for the block suffix
      following statement 0 at 16:37
  --> src/main.rs:16:38
   |
16 |         let p = Parent { count: 42 };
   |                                      ^

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

let p = Parent { count: 42 };
// `p` lives at address 0x1000 and takes up 4 bytes
// The value of `p` is 42 
let c = Child { parent: &p };
// `c` lives at address 0x1010 and takes up 4 bytes
// The value of `c` is 0x1000

Combined { parent: p, child: c }
// The return value lives at address 0x2000 and takes up 8 bytes
// `p` is moved to 0x2000
// `c` is ... ?

что должно произойти к c? Если значение было просто перемещено как p был, потом он будет ссылаться на память, которая больше не гарантируется есть допустимое значение в нем. Любой другой фрагмент кода разрешается хранить значения по адресу 0x1000 памяти. Доступ к памяти, что это было целое число может привести к сбоям и / или ошибкам безопасности, и является одним из основные категории ошибок, которые предотвращает ржавчина.

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

в качестве аналогии, подумайте об этом так: в течение жизни человека, они будут проживайте в разных местах, каждое из которых имеет свой адрес. Один Продолжительность жизни ржавчины беспокоит адрес вы в настоящее время проживают в, не о том, когда вы умрете в будущем (хотя умирая также изменяет ваш адрес). Каждый раз, когда вы двигаетесь, это актуально, потому что ваш адрес больше не действителен.

также важно отметить, что продолжительность жизни не изменить код; ваш код управляет жизнями, ваши жизни не управляют кодом. Этот содержательное высказывание: "жизни описательны, а не предписывающий."

давайте Combined::new С некоторыми номерами строк, которые мы будем использовать чтобы выделить время жизни:
{                                    // 0
    let p = Parent { count: 42 };    // 1
    let c = Child { parent: &p };    // 2
                                     // 3
    Combined { parent: p, child: c } // 4
}                                    // 5

The срок службы бетона на p от 1 до 4 включительно (что я буду представляю как [1,4]). Конкретный срок службы c и [2,4], и конкретный срок службы возвращаемого значения [4,5]. Это возможно иметь конкретные жизни, которые начинаются с нуля - это было бы представление времени жизни параметра в a функция или что-то такое существовал за пределами квартала.

обратите внимание, что время жизни и [2,4], но это относится к значение со сроком жизни [1,4]. Это нормально до тех пор, пока ссылающееся значение становится недействительным до того, как это сделает указанное значение. Этот проблема возникает, когда мы пытаемся вернуть c из блока. Это "сверх-расширьте" продолжительность жизни за своей естественной длиной.

это новое знание должно объяснить первые два примера. Третий один требует смотреть на реализацию Parent::child. Шансы это будет выглядеть примерно так:

impl Parent {
    fn child(&self) -> Child { ... }
}

использует lifetime elision чтобы не писать явную generic параметры времени жизни. Это эквивалентно:

impl Parent {
    fn child<'a>(&'a self) -> Child<'a> { ... }
}

в обоих случаях метод говорит, что a Child структура будет возвращается, который был параметризован с конкретным сроком службы self. Сказал по-другому: Child экземпляр содержит ссылку к Parent который создал его, и поэтому не может жить дольше, чем это Parent экземпляра.

это также позволяет нам понять, что что-то действительно не так с нашим функция создания:

fn make_combined<'a>() -> Combined<'a> { ... }

хотя вы, скорее всего, увидите это написано в другой форме:

impl<'a> Combined<'a> {
    fn new() -> Combined<'a> { ... }
}

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

как это исправить?

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

существует особый случай, когда отслеживание жизни чрезмерно усердно: когда у вас есть что-то, помещенное в кучу. Это происходит при использовании Box<T>, например. В этом случае, структура, которая перемещается содержит указатель на кучу. Заостренное значение останется стабильно, но адрес самого указателя будет двигаться. На практике, это не дело в том, что вы всегда следуете указателю.

The аренда обрешеткой или owning_ref crate are способы представления этого дела, но они требуется, чтобы базовый адрес никогда не двигаться. Это исключает мутацию векторы, которые могут вызвать перераспределение и перемещение в куче значений.

дополнительная информация

после переезда p в структуру, почему компилятор не может получить новая ссылка на p и назначить c в struct?

хотя теоретически это возможно сделать, это приведет к большой сложности и накладным расходам. Каждый раз, когда объект перемещается, компилятор должен будет вставить код, чтобы "исправить" ссылку. Это означало бы, что копирование структуры больше не является очень дешевой операцией, которая просто перемещает некоторые биты. Это может даже означать, что такой код является дорогим, в зависимости от того, как хорошим гипотетическим оптимизатором было бы:

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> {
    // ...
}

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

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

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

получается аренда обрешеткой или owning_ref crate из другого ответа решения этой проблемы тоже. Рассмотрим owning_ref, который имеет специальный объект именно для этой цели: OwningHandle. Чтобы избежать перемещения базового объекта, мы выделяем его в куче с помощью 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 - который включает в себя другой пример и его решение с использованием арендного ящика, который не содержит небезопасных блоков.