Чому я не можу зберігати значення та посилання на це значення в одній структурі?


222

У мене є значення, і я хочу зберігати це значення та посилання на щось усередині цього значення у власному типі:

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

У кожному з цих випадків я отримую помилку, що одне із значень "не живе досить довго". Що означає ця помилка?


1
Для останнього прикладу, визначення Parentта Childмогло б допомогти ...
Матьє М.

1
@MatthieuM. Я обговорював це, але вирішив проти цього, грунтуючись на двох пов'язаних питаннях. Жодне з цих питань не розглядало визначення структури або методу, про яке йдеться, тому я вважав, що найкраще буде імітувати це, щоб люди могли легше співставити це питання з власною ситуацією. Зверніть увагу , що я робити показувати метод підпису у відповідь.
Шепмайстер

Відповіді:


245

Давайте розглянемо просту реалізацію цього :

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 запобігає.

Це саме та проблема, яку життя заважає. Тривалість життя - це трохи метаданих, які дозволяють вам і компілятору знати, як довго значення буде чинним у його поточному місці пам'яті . Це важлива відмінність, оскільки це звичайна помилка, яку роблять новачки Руста. Життя іржі немає є періодом часу між тим, коли об’єкт створений, і коли він знищений!

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

Важливо також зазначити, що життя не змінює ваш код; ваш код контролює життя, ваш час життя не контролює код. Прислівна приказка - "життя є описовим, а не розпорядчим".

Давайте анотуватимемо 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Стабілізований в іржі 1.33, це містить в документації модуля :

Прекрасним прикладом такого сценарію може бути побудова самореференційних структур, оскільки переміщення об'єкта з покажчиками на себе призведе до їх недійсності, що може спричинити невизначеність поведінки.

Важливо зауважити, що "самореференційний" не обов'язково означає використання посилання . Дійсно, на прикладі самореференційної структури конкретно сказано (моє наголос):

Ми не можемо повідомити про це компілятора за допомогою звичайної посилання, оскільки ця модель не може бути описана звичайними правилами запозичення. Натомість ми використовуємо необроблений покажчик , хоча той, який, як відомо, не є нульовим, оскільки ми знаємо, що він вказує на рядок.

Можливість використовувати необроблений покажчик для такої поведінки існує з Rust 1.0. Дійсно, власник-ref та оренда використовують необроблені покажчики під кришкою.

Єдине, що Pin додає до таблиці, - це звичайний спосіб констатувати, що задане значення гарантовано не рухається.

Дивитися також:


1
Чи щось подібне ( is.gd/wl2IAt ) вважається ідіоматичним? Тобто викривати дані методами замість необроблених даних.
Пітер Хол

2
@ ПетерХал впевнений, це просто означає, що Combinedє власником того, Childхто є власником Parent. Це може чи не має сенсу залежно від реальних типів у вас. Повернення посилань на власні внутрішні дані є досить типовим.
Шепмейстер

Яке рішення проблеми купи?
дерекдері

@derekdreery, можливо, ви могли б розширити свій коментар? Чому весь абзац говорить про ящик owning_ref недостатньо?
Шепмайстер

1
@FynnBecker все ще неможливо зберігати посилання та значення на цю посилання. Pinздебільшого це спосіб пізнати безпеку структури, що містить самореференційний вказівник . Можливість використовувати необроблений покажчик для тієї ж мети існує з Rust 1.0.
Шепмайстер

4

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

Це викликає подібні помилки компілятора, пов'язані з життям.

Один із способів вирішити це дуже простим способом - оголосити Sessionзовнішню сторону абонента, а потім анотувати посилання в структурі протягом усього життя, подібно до відповіді у цій публікації форуму користувача Rust User розповідає про ту саму проблему під час інкапсуляції 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 - яка містить інший приклад та його рішення за допомогою ящика прокату, який не містить небезпечних блоків.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.