如何将拥有的盒式结构体引用传递给其他拥有的结构体?(涉及IT技术)

5
我有一个名为 Engine 的东西,它拥有 Worker,我想让 Engine 作为 trait 的引用提供一些 API 给 Worker 使用。API 的实现是使用 Box 分配的,并由 Engine 拥有,因此只要 worker 存在,对它的引用就是稳定和有效的。
但我不知道如何在 Rust 中表达它。
我已阅读了为什么无法在同一结构体中存储值和对该值的引用?,我理解为什么不能传递对所拥有值的引用。然而,在我的情况下,我传递的是指向箱化值的引用,它不会被移动,因此对它的引用必须是稳定的。
以下是不工作的原型:
trait EngineApi {
    fn foo(&self);
}

struct Worker<'a> {
    api: &'a EngineApi,
}
impl<'a> Worker<'a> {
    fn new(engine_api: &'a EngineApi) -> Self {
        Worker { api: engine_api }
    }
}

struct Api;
impl EngineApi for Api {
    fn foo(&self) {} 
}

struct Engine<'a> {
    api: Box<Api>,
    worker: Box<Worker<'a>>,
}

impl<'a> Engine<'a> {
    fn new() -> Self {
        let api = Box::new(Api);
        let worker = Box::new(Worker::new(api.as_ref()));
        Engine { api: api, worker: worker }
    }
}

fn main() {
    let engine = Engine::new();
}

错误:

test.rs:27:37: 27:40 error: `api` does not live long enough
test.rs:27      let worker = Box::new(Worker::new(api.as_ref()));
                                                  ^~~
test.rs:25:19: 29:3 note: reference must be valid for the lifetime 'a as defined on the block at 25:18...
test.rs:25  fn new() -> Self {
test.rs:26      let api = Box::new(Api);
test.rs:27      let worker = Box::new(Worker::new(api.as_ref()));
test.rs:28      Engine { api: api, worker: worker }
test.rs:29  }
test.rs:26:27: 29:3 note: ...but borrowed value is only valid for the block suffix following statement 0 at 26:26
test.rs:26      let api = Box::new(Api);
test.rs:27      let worker = Box::new(Worker::new(api.as_ref()));
test.rs:28      Engine { api: api, worker: worker }
test.rs:29  }
error: aborting due to previous error

我已经阅读了这个问题,并且理解为什么我不能存储对所拥有值的引用——因为当存储到结构体中时,该值被移动,稍后,当结构体本身被移动时也会移动。然而,在我的情况下,该值是封装的,因此引用是稳定的,虽然在Rust中可能无法表达,但它必须是有效的。 - kriomant
在这种情况下,您能否(1)在您的问题中引用此链接并(2)解释您理解了什么以及为什么您认为您的情况不同?这将帮助我们确切地理解需要解释的内容。 - Matthieu M.
1个回答

3
问题在于,在您的示例中,没有将api对象绑定到比创建它的范围更长的生命周期。因此,基本上您需要先创建整个引擎对象,然后Rust才能推导出这些生命周期。但是您无法安全地创建对象而不填写所有字段。但是您可以将worker字段更改为Option并稍后填写:
struct Engine<'a> {
    api: Box<Api>,
    worker: Option<Box<Worker<'a>>>,
}

impl<'a> Engine<'a> {
    fn new() -> Self {
        let api = Box::new(Api);
        Engine { api: api, worker: None }
    }
    fn turn_on(&'a mut self) {
        self.worker = Some(Box::new(Worker::new(self.api.as_ref())));
    }
}

fn main() {
    let mut engine = Engine::new();
    engine.turn_on();
}

engine.turn_on()的调用将锁定对象,以确保它将留在范围内。这样,您甚至不需要框来确保安全性,因为对象将变得不可移动:

struct Engine<'a> {
    api: Api,
    worker: Option<Worker<'a>>,
}

impl<'a> Engine<'a> {
    fn new() -> Self {
        let api = Api;
        Engine { api: api, worker: None }
    }
    fn turn_on(&'a mut self) {
        self.worker = Some(Worker::new(&self.api));
    }
}

fn main() {
    let mut engine = Engine::new();
    engine.turn_on();
}

Rust编译器无法利用对象应该是可移动的这一事实,因为它引用的东西存储在堆上并且至少与对象一样长寿。也许将来有一天会有所改变,但现在你必须使用不安全的代码。


哇。这种自锁机制真是神奇。我花了一点时间来理解turn_on之前(我可以随意移动引擎)和之后(它是固定的)行为差异之间的区别,但我仍然不确定是否已经全部弄清楚。之后它是固定的事实可能会很痛苦(因为没有turn_off)。 - Matthieu M.
1
是的,这个自锁定确实是非常酷的技巧。然而,我不喜欢两阶段初始化,所以我可能会使用Rc共享API。 - kriomant

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接