【发布时间】: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<AppState + 'static> 的框,使这些框无用,因为状态包含可变状态。
说到状态机,这里是:
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<NullState> 开头,这是一个什么都不做的状态。为简洁起见,我省略了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<AppState + 'static>,因为状态包含可变状态,所以使盒子无用。 - 这不是一个有效的结论。具有可变状态与 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