为什么我不能在同一个结构中存储值和对该值的引用?

发布于 2025-01-17 06:33:35 字数 832 浏览 2 评论 0 原文

我有一个值,我想存储该值和对的引用 我自己的类型中该值内的某些内容:

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

在每种情况下,我都会收到一个错误,其中一个值“确实 活得不够长”。这个错误是什么意思?

I have a value and I want to store that value and a reference to
something inside that value in my own type:

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

Sometimes, I have a value and I want to store that value and a reference to
that value in the same structure:

struct Combined<'a>(Thing, &'a Thing);

fn make_combined<'a>() -> Combined<'a> {
    let thing = Thing::new();

    Combined(thing, &thing)
}

Sometimes, I'm not even taking a reference of the value and I get the
same error:

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

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

    Combined(parent, child)
}

In each of these cases, I get an error that one of the values "does
not live long enough". What does this error mean?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

寄意 2025-01-24 06:33:36

导致非常相似的编译器消息的一个稍微不同的问题是对象生命周期依赖性,而不是存储显式引用。一个例子是 ssh2 库。当开发比测试项目更大的东西时,很容易尝试将从该会话获取的 SessionChannel 并排放入一个结构中,从而隐藏实现细节用户。但是,请注意 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 code> 不再存在,但它与我们将使用的 Channel 一起存储。因为 OwningHandle 对象取消引用 Box,而 Box 取消引用 Channel,因此在将其存储在结构中时,我们如此命名。 注意:这只是我的理解。我怀疑这可能不正确,因为它似乎非常接近 讨论OwningHandle不安全

这里一个奇怪的细节是,Session 在逻辑上与 TcpStream 具有类似的关系,就像 ChannelSession 的关系一样,但是它的所有权没有被占用,并且没有类型注释围绕这样做。相反,由用户来处理这个问题,如 握手方法说:

此会话不拥有所提供的套接字的所有权,它是
建议确保套接字在其生命周期内持续存在
会话以确保通信正确执行。

也强烈建议不要使用提供的流
在本届会议期间同时在其他地方进行
干扰协议。

因此,对于TcpStream的使用,完全由程序员来确保代码的正确性。借助 OwningHandle,可以使用 unsafe {} 块来关注“危险魔法”发生的位置。

关于此问题的进一步、更高级别的讨论在此 Rust 用户论坛帖子 - 其中包括一个不同的示例及其使用租赁箱的解决方案,该箱不包含不安全的块。

A slightly different issue which causes very similar compiler messages is object lifetime dependency, rather than storing an explicit reference. An example of that is the ssh2 library. When developing something bigger than a test project, it is tempting to try to put the Session and Channel obtained from that session alongside each other into a struct, hiding the implementation details from the user. However, note that the Channel definition has the 'sess lifetime in its type annotation, while Session doesn't.

This causes similar compiler errors related to lifetimes.

One way to solve it in a very simple way is to declare the Session outside in the caller, and then for annotate the reference within the struct with a lifetime, similar to the answer in this Rust User's Forum post talking about the same issue while encapsulating SFTP. This will not look elegant and may not always apply - because now you have two entities to deal with, rather than one that you wanted!

Turns out the rental crate or the owning_ref crate from the other answer are the solutions for this issue too. Let's consider the owning_ref, which has the special object for this exact purpose:
OwningHandle. To avoid the underlying object moving, we allocate it on the heap using a Box, which gives us the following possible solution:

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

The result of this code is that we can not use the Session anymore, but it is stored alongside with the Channel which we will be using. Because the OwningHandle object dereferences to Box, which dereferences to Channel, when storing it in a struct, we name it as such. NOTE: This is just my understanding. I have a suspicion this may not be correct, since it appears to be quite close to discussion of OwningHandle unsafety.

One curious detail here is that the Session logically has a similar relationship with TcpStream as Channel has to Session, yet its ownership is not taken and there are no type annotations around doing so. Instead, it is up to the user to take care of this, as the documentation of handshake method says:

This session does not take ownership of the socket provided, it is
recommended to ensure that the socket persists the lifetime of this
session to ensure that communication is correctly performed.

It is also highly recommended that the stream provided is not used
concurrently elsewhere for the duration of this session as it may
interfere with the protocol.

So with the TcpStream usage, is completely up to the programmer to ensure the correctness of the code. With the OwningHandle, the attention to where the "dangerous magic" happens is drawn using the unsafe {} block.

A further and a more high-level discussion of this issue is in this Rust User's Forum thread - which includes a different example and its solution using the rental crate, which does not contain unsafe blocks.

月棠 2025-01-24 06:33:36

我发现 Arc (只读)或 Arc (带锁定的读写)模式有时是性能和代码复杂性之间非常有用的权衡(主要是由生命周期注释引起的)。

用于只读访问的 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; // fails, Arc is not 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 (何时或为何应使用互斥量而不是 RwLock?

I've found the Arc (read-only) or Arc<Mutex> (read-write with locking) patterns to be sometimes quite useful tradeoff between performance and code complexity (mostly caused by lifetime-annotation).

Arc for read-only access:

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
}

Arc + Mutex for read-write access:

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

See also RwLock (When or why should I use a Mutex over an RwLock?)

默嘫て 2025-01-24 06:33:36

该答案应该作为已接受答案中提到的板条箱的示例集合。

停下来听听!很可能,您并不真正需要自我引用的箱子。正如您从下面的示例中看到的,它们很快就会变得非常笨重。此外,健全性漏洞仍在被发现,维护这样的板条箱是一个持续的负担。

如果您完全控制所涉及的类型,则更喜欢没有自我参照的设计。使用自引用板条箱的唯一原因是,如果您获得了某个具有 API 的外部库,迫使您进行自引用。


ouroboros

最古老的仍然维护的箱子,有很多下载。

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

impl Example {
    // `new()` is used by `ouroboros`. In real world, you will probably wrap in another struct.
    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()` is used by `self_cell`. In real world, you will probably wrap in another struct.
    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< /code>),虽然您可以为您的类型派生它,但对于外国类型,您就不走运了。突变也是有限的,因为最初的目的是用于零拷贝反序列化。尽管如此,这仍然是一个有用的箱子:

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

This answer is supposed to serve as a collection of examples for the crates mentioned in the accepted answer.

Stop and listen! Chances are, you don't really need self-referential crates. As you can see from the examples below, they can get real unwieldy quickly. In addition, soundness holes are still being discovered, and maintaining such crate is a constant burden.

If you fully control the types involved, prefer a design with no self-referentiality. The only reason to use a self-referential crate is if you are given with some external library that has an API that forces you to be self-referential.


ouroboros

The oldest still maintained crate, with many downloads.

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

impl Example {
    // `new()` is used by `ouroboros`. In real world, you will probably wrap in another struct.
    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

You can see it is very similar to ouroboros, but it is a declarative macro, so its flexibility is limited:

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

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

    impl { Debug }
}

impl Example {
    // `new()` is used by `self_cell`. In real world, you will probably wrap in another struct.
    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

The big disadvantage of this crate is that it cannot be used with third-party types safely. The type that refers the other type must implement a specific trait (Yokeable), and while you can derive it for your types, for foreign types you are out of luck. Mutation is also limited, since original intention was to be used for zero-copy deserialization. Still, this can be a useful crate:

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

This crate was created to suggest a new approach to self-referential structs, based on the self-referentiality of async blocks that the compiler generates for you.

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
        })
    }
}
坚持沉默 2025-01-24 06:33:36

作为 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 变体),因为在我的情况下,“父”对象只是临时的东西(数据库“客户端”对象),我必须构造该对象以传递给“子”对象(数据库“事务”对象),以便我可以运行一些数据库命令。

无论如何,它完成了我需要的封装/样板简化(因为我有许多需要创建事务/“子”对象的函数,现在他们需要的是通用锚对象创建行),同时避免需要使用一个全新的库。

这些是我知道可能相关的库:

但是,我扫描了它们,它们所有似乎都存在这样或那样的问题(多年来没有更新,提出了多个不合理的问题/担忧等),所以我在使用它们时犹豫不决。

因此,虽然这不是通用的解决方案,但我想我会为具有类似用例的人提及它:

  • 调用者只需要返回“子”对象。
  • 但被调用函数需要构造一个“父”对象来执行其功能。
  • 并且借用规则要求“父”对象存储在“make_parent_and_child”函数之外持续存在的位置。 (就我而言,这是一个 start_transaction 函数)

As a newcomer to Rust, I had a case similar to your last example:

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

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

    Combined(parent, child)
}

In the end, I solved it by using this pattern:

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

This is far from a universal solution! But it worked in my case, and only required usage of the main_simple pattern above (not the main_complex variant), because in my case the "parent" object was just something temporary (a database "Client" object) that I had to construct to pass to the "child" object (a database "Transaction" object) so I could run some database commands.

Anyway, it accomplished the encapsulation/simplification-of-boilerplate that I needed (since I had many functions that needed creation of a Transaction/"child" object, and now all they need is that generic anchor-object creation line), while avoiding the need for using a whole new library.

These are the libraries I'm aware of that may be relevant:

However, I scanned through them, and they all seem to have issues of one kind or another (not being updated in years, having multiple unsoundness issues/concerns raised, etc.), so I was hesitant to use them.

So while this isn't as generic of a solution, I figured I would mention it for people with similar use-cases:

  • Where the caller only needs the "child" object returned.
  • But the called-function needs to construct a "parent" object to perform its functions.
  • And the borrowing rules requires that the "parent" object be stored somewhere that persists beyond the "make_parent_and_child" function. (in my case, this was a start_transaction function)
情徒 2025-01-24 06:33:35

让我们看一下这个的简单实现

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 生命周期不是对象被创建的时间间隔
创建和销毁的时间!

打个比方,这样想:在人的一生中,他们会
驻留在许多不同的位置,每个位置都有不同的地址。一个
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 结构将是
返回已用具体生命周期参数化的
自我。换句话说,Child 实例包含一个引用
到创建它的 Parent,因此不能活得比这个更长
实例。

这也让我们认识到,我们的行为确实出了问题。
创建函数:

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

尽管您更有可能看到以不同形式编写的函数:

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

在这两种情况下,都没有通过
争论。这意味着 Combined 的生命周期将是
参数化 with 不受任何限制 - 它可以是任何东西
呼叫者希望如此。这是无意义的,因为调用者
可以指定 'static 生命周期,但无法满足该要求
健康)状况。

我该如何修复它?

最简单和最推荐的解决方案是不要尝试将
这些项目在同一结构中组合在一起。通过这样做,您的
结构嵌套将模仿代码的生命周期。场所类型
将数据一起放入一个结构中,然后提供方法
允许您根据需要获取引用或包含引用的对象。

有一种特殊情况,生命周期跟踪过于热心:
当你有东西放在堆上时。当您使用
例如,Box。在这种情况下,移动的结构
包含一个指向堆的指针。指定值将保持不变
稳定,但是指针本身的地址会移动。在实践中,
这并不重要,因为您始终遵循指针。

有些板条箱提供了表示这种情况的方法,但它们
要求基地址永远不会移动。这排除了变异
向量,这可能会导致重新分配和移动
堆分配的值。

通过 Rental 解决的问题示例:

在其他情况下,您可能希望转向某种类型的引用计数,例如使用 Rc< /a> 或 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();

    // cannot borrow `tricky` as immutable because it is also borrowed as mutable
    // println!("{:?}", tricky);
}

另请参阅:

Pin 怎么样?

Pin,在 Rust 1.33 中稳定,在模块文档中

这种情况的一个主要例子是构建自引用结构,因为移动带有指向自身的指针的对象将使它们无效,这可能会导致未定义的行为。

需要注意的是,“自引用”并不一定意味着使用引用。。事实上,自引用结构的示例< /a> 具体说(强调我的):

我们无法通过普通引用通知编译器,
因为这种模式无法用通常的借用规则来描述。
相反,我们使用原始指针,尽管已知该指针不为空,
因为我们知道它指向字符串。

自 Rust 1.0 以来就已经存在使用原始指针来实现此行为的能力。事实上,拥有引用和租赁在幕后使用原始指针。

Pin 添加到表中的唯一内容是一种声明给定值保证不会移动的常用方法。

另请参阅:

Let's look at a simple implementation of this:

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

This will fail with the error:

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`

To completely understand this error, you have to think about how the
values are represented in memory and what happens when you move
those values. Let's annotate Combined::new with some hypothetical
memory addresses that show where values are located:

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 ... ?

What should happen to child? If the value was just moved like parent
was, then it would refer to memory that no longer is guaranteed to
have a valid value in it. Any other piece of code is allowed to store
values at memory address 0x1000. Accessing that memory assuming it was
an integer could lead to crashes and/or security bugs, and is one of
the main categories of errors that Rust prevents.

This is exactly the problem that lifetimes prevent. A lifetime is a
bit of metadata that allows you and the compiler to know how long a
value will be valid at its current memory location. That's an
important distinction, as it's a common mistake Rust newcomers make.
Rust lifetimes are not the time period between when an object is
created and when it is destroyed!

As an analogy, think of it this way: During a person's life, they will
reside in many different locations, each with a distinct address. A
Rust lifetime is concerned with the address you currently reside at,
not about whenever you will die in the future (although dying also
changes your address). Every time you move it's relevant because your
address is no longer valid.

It's also important to note that lifetimes do not change your code; your
code controls the lifetimes, your lifetimes don't control the code. The
pithy saying is "lifetimes are descriptive, not prescriptive".

Let's annotate Combined::new with some line numbers which we will use
to highlight lifetimes:

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

The concrete lifetime of parent is from 1 to 4, inclusive (which I'll
represent as [1,4]). The concrete lifetime of child is [2,4], and
the concrete lifetime of the return value is [4,5]. It's
possible to have concrete lifetimes that start at zero - that would
represent the lifetime of a parameter to a function or something that
existed outside of the block.

Note that the lifetime of child itself is [2,4], but that it refers
to
a value with a lifetime of [1,4]. This is fine as long as the
referring value becomes invalid before the referred-to value does. The
problem occurs when we try to return child from the block. This would
"over-extend" the lifetime beyond its natural length.

This new knowledge should explain the first two examples. The third
one requires looking at the implementation of Parent::child. Chances
are, it will look something like this:

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

This uses lifetime elision to avoid writing explicit generic
lifetime parameters
. It is equivalent to:

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

In both cases, the method says that a Child structure will be
returned that has been parameterized with the concrete lifetime of
self. Said another way, the Child instance contains a reference
to the Parent that created it, and thus cannot live longer than that
Parent instance.

This also lets us recognize that something is really wrong with our
creation function:

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

Although you are more likely to see this written in a different form:

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

In both cases, there is no lifetime parameter being provided via an
argument. This means that the lifetime that Combined will be
parameterized with isn't constrained by anything - it can be whatever
the caller wants it to be. This is nonsensical, because the caller
could specify the 'static lifetime and there's no way to meet that
condition.

How do I fix it?

The easiest and most recommended solution is to not attempt to put
these items in the same structure together. By doing this, your
structure nesting will mimic the lifetimes of your code. Place types
that own data into a structure together and then provide methods that
allow you to get references or objects containing references as needed.

There is a special case where the lifetime tracking is overzealous:
when you have something placed on the heap. This occurs when you use a
Box<T>, for example. In this case, the structure that is moved
contains a pointer into the heap. The pointed-at value will remain
stable, but the address of the pointer itself will move. In practice,
this doesn't matter, as you always follow the pointer.

Some crates provide ways of representing this case, but they
require that the base address never move. This rules out mutating
vectors, which may cause a reallocation and a move of the
heap-allocated values.

Examples of problems solved with Rental:

In other cases, you may wish to move to some type of reference-counting, such as by using Rc or Arc.

More information

After moving parent into the struct, why is the compiler not able to get a new reference to parent and assign it to child in the struct?

While it is theoretically possible to do this, doing so would introduce a large amount of complexity and overhead. Every time that the object is moved, the compiler would need to insert code to "fix up" the reference. This would mean that copying a struct is no longer a very cheap operation that just moves some bits around. It could even mean that code like this is expensive, depending on how good a hypothetical optimizer would be:

let a = Object::new();
let b = a;
let c = b;

Instead of forcing this to happen for every move, the programmer gets to choose when this will happen by creating methods that will take the appropriate references only when you call them.

A type with a reference to itself

There's one specific case where you can create a type with a reference to itself. You need to use something like Option to make it in two steps though:

#[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);
}

This does work, in some sense, but the created value is highly restricted - it can never be moved. Notably, this means it cannot be returned from a function or passed by-value to anything. A constructor function shows the same problem with the lifetimes as above:

fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }

If you try to do this same code with a method, you'll need the alluring but ultimately useless &'a self. When that's involved, this code is even more restricted and you will get borrow-checker errors after the first method call:

#[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();

    // cannot borrow `tricky` as immutable because it is also borrowed as mutable
    // println!("{:?}", tricky);
}

See also:

What about Pin?

Pin, stabilized in Rust 1.33, has this in the module documentation:

A prime example of such a scenario would be building self-referential structs, since moving an object with pointers to itself will invalidate them, which could cause undefined behavior.

It's important to note that "self-referential" doesn't necessarily mean using a reference. Indeed, the example of a self-referential struct specifically says (emphasis mine):

We cannot inform the compiler about that with a normal reference,
since this pattern cannot be described with the usual borrowing rules.
Instead we use a raw pointer, though one which is known to not be null,
since we know it's pointing at the string.

The ability to use a raw pointer for this behavior has existed since Rust 1.0. Indeed, owning-ref and rental use raw pointers under the hood.

The only thing that Pin adds to the table is a common way to state that a given value is guaranteed to not move.

See also:

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文