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

Вопрос или проблема

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

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

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

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

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` живет по адресу 0x1000 и занимает 4 байта
// Значение `parent` равно 42 
let child = Child { parent: &parent };
// `child` живет по адресу 0x1010 и занимает 4 байта
// Значение `child` равно 0x1000

Combined { parent, child }
// Возвратное значение живет по адресу 0x2000 и занимает 8 байт
// `parent` перемещается на 0x2000
// `child` это ... ?

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

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

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

Примеры проблем, решенных с помощью Rental:

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

Если вы попытаетесь сделать то же самое с методом, вам понадобится манящая, но в конечном итоге бесполезная &'a self. Когда это вовлечено, этот код еще больше ограничен, и вы получите ошибки проверки заимствования после первого вызова метода:

#[derive(Debug)]
struct WhatAboutThis<'a> {
    name: String,
    nickname: Option<&'a str>,
}

impl<'a> WhatAboutThis<'a> {
    fn tie_the_knot(&'a mut self) {
       self.nickname = Some(&self.name[..4]); 
    }
}

fn main() {
    let mut tricky = WhatAboutThis {
        name: "Annabelle".to_string(),
        nickname: None,
    };
    tricky.tie_the_knot();

    // нельзя заимствовать `tricky` как неизменяемый, поскольку он также заимствован как изменяемый
    // println!("{:?}", tricky);
}

См. также:

Что насчет Pin?

Pin, стабилизированный в Rust 1.33, имеет это в документации модуля:

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

Важно отметить, что “самоссылочный” не обязательно означает использование ссылки. Действительно, пример самоссылочной структуры конкретно говорит (выделение мое):

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

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

Единственное, что Pin добавляет к этому, – это общий способ указать, что данное значение гарантировано не переместится.

См. также:

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

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

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

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

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

Arc для доступа только на чтение:

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; // неудача, Arc не является DerefMut
}

Arc + Mutex для доступа на чтение/запись:

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> {
    // создаем родителя, затем храним его в объекте якоря, на который мы получили изменяемую ссылку
    *anchor = DataAnchorFor1::holding(Parent::new());

    // теперь извлекаем родителя из хранилища, которое мы назначили на предыдущей строке
    let parent = anchor.val1.as_mut().unwrap();

    // теперь продолжаем обычный код, возвращая только ребенка
    // (родитель уже доступен вызывающей стороне через объект якоря)
    let child = parent.child();
    child
}

// это универсальная структура, которую мы можем определить один раз и использовать каждый раз, когда нам нужен этот паттерн
// (ее также можно расширить, чтобы иметь несколько слотов, естественно)
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) }
    }
}

// для моего случая, это было все, что мне нужно
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);
}

// но если доступ к данным родителя позже необходим, вы можете использовать это
fn main_complex() {
    let anchor = DataAnchorFor1::empty();

    // если вы хотите использовать объект родителя (который хранится в якоре), вы должны...
    // ...обернуть обработку, связанную с дочерним элементом, в новую область видимости, чтобы изменяемая ссылка на якорь...
    // ...была сброшена в конце, позволяя нам получить доступ к anchor.val1 (родителю) напрямую
    let child_processing_result = {
        let child = make_parent_and_child(&mut anchor);
        // выполните желаемую обработку с дочерним элементом здесь (избегая цепочки ссылок...
        // ...обратно к данным якоря, если вам нужно получить доступ к данным родителя позже)
        do_some_processing(child)
    };

    // теперь, когда область видимости закончилась, мы можем получить доступ к данным родителя напрямую
    // и распечатать соответствующие данные как для родителя, так и для ребенка (откорректируйте для вашего случая)
    let parent = anchor.val1.unwrap();
    println!("Parent:{} ChildProcessingResult:{}", parent, child_processing_result);
}

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

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

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

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

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

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

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

Остановитесь и слушайте! Скорее всего, вам не нужны крейты с самоссылочностью. Как вы можете увидеть из приведенных ниже примеров, они могут быстро стать непрактичными. Кроме того, в них по-прежнему продолжают обнаруживать проблемы со звуковостью, и поддержка таких крейтов – это постоянное бремя.

Если вы полностью контролируете вовлеченные типы, лучше выбрать проект без самоссылочности. Единственная причина использовать самоссылочный крейт – это если вам предоставили какую-то внешнюю библиотеку, API которой заставляет вас быть самоссылочным.


ouroboros

Самый старый все еще поддерживаемый крейт, с множеством загрузок.

#[ouroboros::self_referencing]
struct Example {
    source: String,
    #[borrows(source)]
    borrowed: &'this str,
}

impl Example {
    // `new()` используется `ouroboros`. В реальном мире, вы, вероятно, обернете это в другую структуру.
    fn construct(source: String) -> Self {
        Self::new(source, |source| source.as_str())
    }

    fn take_borrowed(&mut self, n: usize) -> &str {
        self.with_borrowed_mut(|borrowed| {
            let (head, tail) = borrowed.split_at(n);
            *borrowed = tail;
            head
        })
    }

    fn reset_borrowed(&mut self) {
        self.with_mut(|fields| *fields.borrowed = fields.source.as_str());
    }
}

self_cell

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

type BorrowedStr<'a> = &'a str;

self_cell::self_cell! {
    struct Example {
        owner: String,
        #[covariant]
        dependent: BorrowedStr,
    }

    impl { Debug }
}

impl Example {
    // `new()` используется `self_cell`. В реальном мире, вы, вероятно, обернете это в другую структуру.
    fn construct(source: String) -> Self {
        Self::new(source, |source| source.as_str())
    }

    fn take_borrowed(&mut self, n: usize) -> &str {
        self.with_dependent_mut(|_, dependent| {
            let (head, tail) = dependent.split_at(n);
            *dependent = tail;
            head
        })
    }

    fn reset_borrowed(&mut self) {
        self.with_dependent_mut(|owner, dependent| *dependent = owner.as_str());
    }
}

yoke

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

use yoke::Yoke;

struct Example(Yoke<&'static str, String>);

impl Example {
    fn new(source: String) -> Self {
        Self(Yoke::attach_to_cart(source, |source| source))
    }

    fn peek(&self, n: usize) -> &str {
        &self.0.get()[..n]
    }

    fn consume(&mut self, n: usize) {
        self.0.with_mut(move |borrowed| *borrowed = &borrowed[n..]);
    }
}

nolife

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

struct Source(String);

struct SourceFamily;
impl<'a> nolife::Family<'a> for SourceFamily {
    type Family = &'a str;
}

fn create_scope(source: String) -> impl nolife::TopScope<Family = SourceFamily> {
    nolife::scope!({ freeze_forever!(&mut source.as_str()) })
}

struct Example(nolife::BoxScope<SourceFamily>);

impl Example {
    fn new(source: String) -> Self {
        Self(nolife::BoxScope::new_dyn(create_scope(source)))
    }

    fn parse_and_advance(&mut self) -> u32 {
        self.0.enter(|borrowed| {
            let result = borrowed[..4].parse().unwrap();
            *borrowed = &borrowed[4..];
            result
        })
    }
}

Ответ или решение

Неудачи при попытке сохранить значение и ссылку на это значение в одной структуре в Rust могут быть результатом mismanagement с учетом времени жизни (lifetimes) данных. Важным аспектом экосистемы программирования на Rust является строгая система типов и механизм заимствования, которые нацелены на предотвращение ошибок, связанных с обращением к некорректным адресам памяти. Рассмотрим подробнее, почему возникновение ошибки "значение не живет достаточно долго" приводит к тому, что невозможно одновременно сохранять значение и ссылку на него в одной структуре.

Что означает ошибка "значение не живет достаточно долго"?

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

Проблема из-за перемещения значений

Рассмотрим следующий пример:

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

В данном случае, когда происходит возвращение структуры Combined, компилятор обнаруживает, что поле child хранит ссылку на parent, который уже был перемещен в структуру Combined. Ссылка на перемещенное значение больше не является действительной, так как в Rust перемещение значения подразумевает, что оригинал больше не доступен. Это потенциально может привести к обращению к несуществующим данным, что и пытается предотвратить структура времени жизни.

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

Воображая, что в памяти parent располагается, например, по адресу 0x1000, а child по адресу 0x2000, адрес 0x1000 перестает быть доступным после перемещения parent. Таким образом, сохранение ссылки на parent, которая переместилась в новую структуру, становится небезопасным.

Как избежать этих ошибок

  1. Разделение значений и ссылок: Лучше хранить значения и ссылки отдельно и предоставить методы, которые могут возвращать ссылки или изменяемые данные по необходимости. Этим вы избежите конфликтов, связанных с временем жизни.

  2. Использование обработки на куче: При необходимости сохранить значение и ссылку на это значение, можно рассмотреть возможность использования указателей на кучу, таких как Box или Rc. Например:

use std::rc::Rc;

struct Parent {
    count: u32,
}

struct Child {
    parent: Rc<Parent>,
}

struct Combined {
    parent: Rc<Parent>,
    child: Child,
}

fn main() {
    let parent = Rc::new(Parent { count: 42 });
    let child = Child { parent: Rc::clone(&parent) };

    let combined = Combined {
        parent,
        child,
    };
}

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

Заключение

Rust предоставляет мощные механизмы для управления памятью и предотвращения ошибок, связанных с временем жизни данных. Понимание принципов работы времени жизни и заимствования поможет избежать распространенных проблем, связанных с сохранением значений и ссылок в одной структуре. Следуя описанным рекомендациям, вы сможете безопасно манипулировать данными, сохраняя их целостность и безопасность в вашем коде.

Оцените материал
Добавить комментарий

Капча загружается...