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

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

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

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

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

4b9b3361

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

Примеры проблем, решаемых с помощью проката:

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

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

Ответ 2

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

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

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

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

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