Rust, how to return reference to something in a struct that lasts as long as the struct?

为君一笑 提交于 2020-12-07 03:40:51

问题


I am porting a compiler I wrote to Rust. In it, I have an enum Entity which represents things like functions and variables:

pub enum Entity<'a> {
  Variable(VariableEntity),
  Function(FunctionEntity<'a>)
  // Room for more later.
}

I then have a struct Scope which is responsible for holding on to these entities in a hash map, where the key is the name given by the programmer to the entity. (For example, declaring a function named sin would put an Entity into the hash map at the key sin.)

pub struct Scope<'a> {
    symbols: HashMap<String, Entity<'a>>,
    parent: Option<&'a Scope<'a>>
}

I would like to be able to get read-only references to the objects in the HashMap so that I can refer to it from other data structures. For example, when I parse a function call, I want to be able to store a reference to the function that is being called instead of just storing the name of the function and having to look up the reference every time I need the actual Entity object corresponding to the name. To do so, I have made this method:

impl<'a> Scope<'a> {
  pub fn lookup(&self, symbol: &str) -> Option<&'a Entity<'a>> {
    let result = self.symbols.get(symbol);
    match result {
      Option::None => match self.parent {
        Option::None => Option::None,
        Option::Some(parent) => parent.lookup(symbol),
      },
      Option::Some(_value) => result
    }
  }
}

However, this results in a compilation error:

error[E0495]: cannot infer an appropriate lifetime for autoref due to conflicting requirements
  --> src/vague/scope.rs:29:31
   |
29 |     let result = self.symbols.get(symbol);
   |                               ^^^
   |
note: first, the lifetime cannot outlive the anonymous lifetime #1 defined on the method body at 28:3...
  --> src/vague/scope.rs:28:3
   |
28 | /   pub fn lookup(&self, symbol: &str) -> Option<&'a Entity<'a>> {
29 | |     let result = self.symbols.get(symbol);
30 | |     match result {
31 | |       Option::None => match self.parent {
...  |
36 | |     }
37 | |   }
   | |___^
note: ...so that reference does not outlive borrowed content
  --> src/vague/scope.rs:29:18
   |
29 |     let result = self.symbols.get(symbol);
   |                  ^^^^^^^^^^^^
note: but, the lifetime must be valid for the lifetime 'a as defined on the impl at 9:6...
  --> src/vague/scope.rs:9:6
   |
9  | impl<'a> Scope<'a> {
   |      ^^
   = note: ...so that the expression is assignable:
           expected std::option::Option<&'a vague::entity::Entity<'a>>
              found std::option::Option<&vague::entity::Entity<'_>>

Things I Tried

There are several ways to make the compilation error go away, but none of them give the behavior I want. First, I can do this:

  pub fn lookup(&self, symbol: &str) -> Option<&Entity<'a>> {

But this means the reference will not live long enough, so I can't put it into a struct or any other kind of storage that will outlive the scope that lookup is called from. Another solution was this:

  pub fn lookup(&self, symbol: &str) -> Option<&'a Entity> {

Which I do not understand why it could compile. As part of the struct definition, things inside Entity objects in the hash map must live at least as long as the scope, so how can the compiler allow the return type to be missing that? Additionally, why would the addition of <'a> result in the previous compiler error, since the only place the function is getting Entitys from is from the hash map, which is defined as having a value type of Entity<'a>. Another bad fix I found was:

  pub fn lookup(&'a self, symbol: &str) -> Option<&'a Entity<'a>> {

Which would mean that lookup can only be called once, which is obviously a problem. My previous understanding was incorrect, but the problem still remains that requiring the reference to self to have the same lifetime as the whole object severely restricts the code in that I can't call this method from a reference with any shorter lifetime, e.g. one passed in as a function argument or one created in a loop.

How can I go about fixing this? Is there some way I can fix the function as I have it now, or do I need to implement the behavior I'm looking for in an entirely different way?


回答1:


Here's the signature you want:

pub fn lookup(&self, symbol: &str) -> Option<&'a Entity<'a>>

Here's why it can't work: it returns a reference that borrows an Entity for longer than lookup initially borrowed the Scope. This isn't illegal, but it means that the reference lookup returns can't be derived from the self reference. Why? Because given the above signature, this is valid code:

let sc = Scope { ... };
let foo = sc.lookup("foo");
drop(sc);
do_something_with(foo);

This code compiles because it has to: there is no lifetime constraint that the compiler could use to prove it wrong, because the lifetime of foo isn't coupled to the borrow of sc. But clearly, if lookup were implemented the way you first tried, foo would contain a dangling pointer after drop(sc), which is why the compiler rejected it.

You must redesign your data structures to make the given signature for lookup work. It's not clear how best to do this given the code in the question, but here are some ideas:

  • Decouple the lifetimes in Scope so that the parent is borrowed for a different lifetime than the symbols. Then have lookup take &'parent self. This probably will not work by itself, depending on what you need to do with the Entitys, but you may need to do it anyway if you need to distinguish between the lifetimes of different data.

    pub struct Scope<'parent, 'sym> {
        symbols: HashMap<String, Entity<'sym>>,
        parent: Option<&'parent Scope<'parent, 'sym>>,
    }
    
    impl<'parent, 'sym> Scope<'parent, 'sym> {
        pub fn lookup(&'parent self, symbol: &str) -> Option<&'parent Entity<'sym>> {
            /* ... */
        }
    }
    
  • Store your Scopes and/or your Entitys in an arena. An arena can give out references that outlive the self-borrow, as long as they don't outlive the arena data structure itself. The tradeoff is that nothing in the arena will be deallocated until the whole arena is destroyed. It's not a substitute for garbage collection.

  • Use Rc or Arc to store your Scopes and/or your Entitys and/or whatever data Entity stores that contains references. This is one way to get rid of the lifetime parameter completely, but it comes with a small runtime cost.



来源:https://stackoverflow.com/questions/57048384/rust-how-to-return-reference-to-something-in-a-struct-that-lasts-as-long-as-the

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!