【问题标题】:Getting around Rust ownership problems when using state machine pattern使用状态机模式时解决 Rust 所有权问题
【发布时间】:2018-04-16 20:19:56
【问题描述】:

这个问题是关于在 Rust 中为视频游戏实现状态机时可能出现的特定所有权模式,其中状态可以持有对“全局”借用上下文的引用,并且状态机在哪里拥有它们的状态。我试图在激发问题的同时尽可能多地删减细节,但这是一个相当大且纠结的问题。

这是状态特征:

pub trait AppState<'a> {
    fn update(&mut self, Duration) -> Option<Box<AppState<'a> + 'a>>;
    fn enter(&mut self, Box<AppState<'a> + 'a>);
    //a number of other methods
}

我正在使用盒装 trait 对象而不是枚举来实现状态,因为我希望有很多状态。状态在其更新方法中返回Some(State),以使它们拥有的状态机切换到新状态。我添加了一个生命周期参数,因为没有它,编译器将生成类型为:Box&lt;AppState + 'static&gt; 的框,使这些框无用,因为状态包含可变状态。

说到状态机,这里是:

pub struct StateMachine<'s> {
    current_state: Box<AppState<'s> + 's>,
}

impl<'s> StateMachine<'s> {
    pub fn switch_state(&'s mut self, new_state: Box<AppState<'s> + 's>) -> Box<AppState<'s> + 's> {
        mem::replace(&mut self.current_state, new_state);
    }
}

状态机始终具有有效状态。默认情况下,它以Box&lt;NullState&gt; 开头,这是一个什么都不做的状态。为简洁起见,我省略了NullState。就其本身而言,这似乎编译得很好。

InGame 状态旨在实现一个基本的游戏场景:

type TexCreator = TextureCreator<WindowContext>;

pub struct InGame<'tc> {
    app: AppControl,
    tex_creator: &'tc TexCreator,

    tileset: Tileset<'tc>,
}

impl<'tc> InGame<'tc> {
    pub fn new(app: AppControl, tex_creator: &'tc TexCreator) -> InGame<'tc> {
        // ... load tileset ...

        InGame {
            app,
            tex_creator,
            tileset,
        }
    }
}

这个游戏依赖于 Rust SDL2。这组特定的绑定要求纹理由TextureCreator 创建,并且纹理的寿命不超过其创建者。纹理需要一个生命周期参数来确保这一点。 Tileset 拥有一个纹理,因此导出了这个需求。这意味着我不能在状态本身中存储TextureCreator(尽管我愿意),因为可变借用的InGame 可能会将纹理创建者移出。因此,纹理创建者在main 中拥有,当我们创建我们的主状态时,对它的引用被传递给:

fn main() {
    let app_control = // ...
    let tex_creator = // ...
    let in_game = Box::new(states::InGame::new(app_control, &tex_creator));
    let state_machine = states::StateMachine::new();
    state_machine.switch_state(in_game);
}

我觉得这个程序应该是有效的,因为我已经确保tex_creator 比任何可能的状态都长,并且该状态机是寿命最短的变量。但是,我收到以下错误:

error[E0597]: `state_machine` does not live long enough
  --> src\main.rs:46:1
   |
39 |     state_machine.switch_state( in_game );
   |     ------------- borrow occurs here
...
46 | }
   | ^ `state_machine` dropped here while still borrowed
   |
   = note: values in a scope are dropped in the opposite order they are created

这对我来说没有意义,因为state_machine只是被方法调用借用了,但是编译器说方法结束时它仍然是借用的。我希望它能让我在错误消息中追踪借用者的身份——我不明白为什么当方法返回时没有返回借用。

基本上,我想要以下内容:

  • 状态由 trait 实现。
  • 状态归状态机所有。
  • 状态能够包含对任意非静态数据的引用,其生命周期大于状态机的生命周期。
  • 当一个状态被换出时,旧盒子仍然有效,以便它可以移动到新状态的构造函数中。这将允许新状态切换回之前的状态,而无需重新构建。
  • 状态可以通过从“更新”返回新状态来发出状态更改的信号。旧状态必须能够在自身内部构建这个新状态。

是否可以满足这些约束条件,如果可以,如何满足?

对于这个冗长的问题以及我可能遗漏了一些明显的事情的可能性,我深表歉意,因为在上面的实现中做出了许多决定,我不确定我是否理解生命周期的语义。我试图在网上搜索这种模式的例子,但它似乎比我见过的玩具例子更复杂和受限制。

【问题讨论】:

  • Box&lt;AppState + 'static&gt;,因为状态包含可变状态,所以使盒子无用。 - 这不是一个有效的结论。具有可变状态与 trait 对象中包含的生命周期无关。
  • 回复:“我正在使用盒装 trait 对象而不是枚举来实现状态,因为我希望它们有很多”——这是不合理的。 trait 对象和 sum 类型之间的权衡并没有真正随着变体的数量而改变。它更多的是关于你想要什么保证。 (这并不是说我认为你错了——只是对措辞有点挑剔。)
  • @Shepmaster 你是对的。我想我应该说的是盒子里的状态持有的借用数据的生命周期小于'static。如果状态包含拥有的简单不可变状态,我可以静态定义它们。我仍在尝试了解生命周期,因此此更正也可能是错误的。
  • @trentcl 主要的权衡是,当我调用 AppState 上定义的各个方法时,我必须匹配所有可能的状态类型。因此,如果在 AppState 中枚举了 20 个状态,并且 AppState 的公共接口有 10 个方法,那么我需要在 20 个状态中的每一个上进行 10 个匹配,以将静态分派到适当的特定于状态的方法实现。我可能遗漏了一些东西,但这是我的理解。编辑:虽然说实话,现在我想起来了,预先定义应用程序状态的简单性和静态方式有点吸引人。
  • 当然,但无论如何你都必须写下所有这些行为,对吧?对于 trait 对象,它只是分成 20 个不同的impls,每个方法有 10 个方法,而不是单个 impl,其中包含 10 个方法,每个方法都有一个 20 臂 match 表达式。更不用说特征对象具有运行时成本,并且不能具有采用Self 或泛型超过类型的方法。当您不能或不需要知道您正在处理的变体时,特征对象很好,但如果您可能需要 downcast 到具体类型,则最好使用枚举。跨度>

标签: rust state-machine lifetime ownership-semantics


【解决方案1】:

StateMachine::switch_state 中,您想在&amp;mut self 上使用's 生命周期; 's 代表状态借用资源的生命周期,而不是状态机的生命周期。请注意,通过这样做,self 的类型以's 结束两次:完整类型为&amp;'s mut StateMachine&lt;'s&gt;;你只需要在StateMachine 上使用's不是在参考上。

在可变引用 (&amp;'a mut T) 中,Tinvariant,因此 's 也是不变的。这意味着编译器认为状态机与它借用的东西具有相同的生命周期。因此,在调用switch_state 之后,编译器认为状态机最终借用了自己

简而言之,将&amp;'s mut self改为&amp;mut self

impl<'s> StateMachine<'s> {
    pub fn switch_state(&mut self, new_state: Box<AppState<'s> + 's>) -> Box<AppState<'s> + 's> {
        mem::replace(&mut self.current_state, new_state)
    }
}

您还需要将main 中的state_machine 声明为可变:

let mut state_machine = states::StateMachine::new();

【讨论】:

  • 太好了,非常感谢!您的更改修复了它,您的解释非常好。看来我需要阅读更多关于方差的信息。我以前看过那个页面,今天早些时候我一直在努力解决它,但我没有意识到它与这个问题有关。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-02-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-09-23
  • 2013-06-24
相关资源
最近更新 更多