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

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

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

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

4 ответа

Решение

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

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

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

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

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

Давайте аннотировать Combined::new с некоторыми номерами строк, которые мы будем использовать для выделения времени жизни:

{                                    // 0
    let p = Parent { count: 42 };    // 1
    let c = Child { parent: &p };    // 2
                                     // 3
    Combined { parent: p, child: c } // 4
}                                    // 5

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

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

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

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

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

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

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

я нашел Arc(только для чтения) или Arc<Mutex>(чтение-запись с блокировкой) иногда могут быть весьма полезным компромиссом между производительностью и сложностью кода (в основном из-за прижизненной аннотации).

Арка:

      use std::sync::Arc;

struct Parent {
    child: Arc<Child>,
}
struct Child {
    value: u32,
}
struct Combined(Parent, Arc<Child>);

fn main() {
    let parent = Parent { child: Arc::new(Child { value: 42 }) };
    let child = parent.child.clone();
    let combined = Combined(parent, child.clone());

    assert_eq!(combined.0.child.value, 42);
    assert_eq!(child.value, 42);
    // combined.0.child.value = 50; // fails, Arc is not DerefMut
}

Дуга + Мьютекс:

      use std::sync::{Arc, Mutex};

struct Child {
    value: u32,
}
struct Parent {
    child: Arc<Mutex<Child>>,
}
struct Combined(Parent, Arc<Mutex<Child>>);

fn main() {
    let parent = Parent { child: Arc::new(Mutex::new(Child {value: 42 }))};
    let child = parent.child.clone();
    let combined = Combined(parent, child.clone());

    assert_eq!(combined.0.child.lock().unwrap().value, 42);
    assert_eq!(child.lock().unwrap().value, 42);
    child.lock().unwrap().value = 50;
    assert_eq!(combined.0.child.lock().unwrap().value, 50);
}

Смотрите также RwLock(Когда или почему я должен использовать Mutex вместо RwLock?)

Как новичок в Rust, у меня был случай, похожий на ваш последний пример:

      struct Combined<'a>(Parent, Child<'a>);

fn make_combined<'a>() -> Combined<'a> {
    let parent = Parent::new();
    let child = parent.child();

    Combined(parent, child)
}

В конце концов, я решил это, используя этот шаблон:

      fn make_parent_and_child<'a>(anchor: &'a mut DataAnchorFor1<Parent>) -> Child<'a> {
    // construct parent, then store it in anchor object the caller gave us a mut-ref to
    *anchor = DataAnchorFor1::holding(Parent::new());

    // now retrieve parent from storage-slot we assigned to in the previous line
    let parent = anchor.val1.as_mut().unwrap();

    // now proceed with regular code, except returning only the child
    // (the parent can already be accessed by the caller through the anchor object)
    let child = parent.child();
    child
}

// this is a generic struct that we can define once, and use whenever we need this pattern
// (it can also be extended to have multiple slots, naturally)
struct DataAnchorFor1<T> {
    val1: Option<T>,
}
impl<T> DataAnchorFor1<T> {
    fn empty() -> Self {
        Self { val1: None }
    }
    fn holding(val1: T) -> Self {
        Self { val1: Some(val1) }
    }
}

// for my case, this was all I needed
fn main_simple() {
    let anchor = DataAnchorFor1::empty();
    let child = make_parent_and_child(&mut anchor);
    let child_processing_result = do_some_processing(child);
    println!("ChildProcessingResult:{}", child_processing_result);
}

// but if access to parent-data later on is required, you can use this
fn main_complex() {
    let anchor = DataAnchorFor1::empty();
    
    // if you want to use the parent object (which is stored in anchor), you must...
    // ...wrap the child-related processing in a new scope, so the mut-ref to anchor...
    // ...gets dropped at its end, letting us access anchor.val1 (the parent) directly
    let child_processing_result = {
        let child = make_parent_and_child(&mut anchor);
        // do the processing you want with the child here (avoiding ref-chain...
        // ...back to anchor-data, if you need to access parent-data afterward)
        do_some_processing(child)
    };

    // now that scope is ended, we can access parent data directly
    // so print out the relevant data for both parent and child (adjust to your case)
    let parent = anchor.val1.unwrap();
    println!("Parent:{} ChildProcessingResult:{}", parent, child_processing_result);
}

Это далеко не универсальное решение! Но это сработало в моем случае и требовало только использованияmain_simpleрисунок выше (не тотmain_complexвариант), потому что в моем случае «родительский» объект был чем-то временным (объект «Клиент» базы данных), который мне пришлось создать для передачи «дочернему» объекту (объекту «Транзакция» базы данных), чтобы я мог запустить некоторые команды базы данных.

В любом случае, он выполнил инкапсуляцию/упрощение шаблона, в котором я нуждался (поскольку у меня было много функций, которые требовали создания объекта Transaction/«child», и теперь все, что им нужно, это эта общая строка создания объекта привязки), избегая необходимость использования совершенно новой библиотеки.

Это библиотеки, о которых я знаю, которые могут иметь значение:

Тем не менее, я просмотрел их, и у всех у них, похоже, есть проблемы того или иного рода (не обновлялись годами, возникали многочисленные проблемы / проблемы с несостоятельностью и т. д.), поэтому я не решался их использовать.

Так что, хотя это не такое универсальное решение, я решил упомянуть его для людей с похожими вариантами использования:

  • Если вызывающей стороне нужен только возвращаемый "дочерний" объект.
  • Но вызываемая функция должна создать «родительский» объект для выполнения своих функций.
  • А правила заимствования требуют, чтобы «родительский» объект хранился где-то вне функции «make_parent_and_child». (в моем случае это былstart_transactionфункция)
Другие вопросы по тегам