在一系列编程课程的背景下,我决定使用Rust而不是C作为支持编程语言,这些课程的一个方面致力于依赖于接口(dyn traits)的OOP(动态分派):我知道OOP并不适合现代语言和方法,但是现有的代码库和90年代以来团队的习惯仍然存在,以至于学生们必须至少"意识到"这种范式(即使我们不鼓励在新的开发中使用它)。
在this playground中,显示了一个最小的示例,该示例的灵感来自以前用C完成的练习(围绕这段摘录还有许多其他内容)在抽象级别上,Entity
有一个内部状态(为了简单起见,这里是一个位置)和几个负责各种行为的动态组件(绘图、动画、对事件的React...)。这些动态组件实现一些预定义的接口(动态特性),并且可以在应用程序级别自由定义(抽象层不必知道这些组件的细节)。这些组件中的一些可以具有一些内部数据,这些数据甚至可以被变异。例如,在这个最小的代码中,Shape
组件主要用于绘图(实体或此组件通常不需要可变操作),但Animator
组件可能会导致实体(比如它的位置)、组件本身甚至其他组件(例如,更改下一个绘图的颜色)发生变化。
按照注解中的要求,以下是内联代码:
mod common {
pub trait Shape {
fn draw(
&self,
entity: &Entity,
);
fn change_color(
&mut self,
color: String,
);
}
pub trait Animator {
fn animate(
&mut self,
entity: &mut Entity,
);
}
#[derive(Debug)]
pub struct Pos {
pub x: f64,
pub y: f64,
}
pub struct Entity {
pos: Pos,
shape: Box<dyn Shape>,
animator: Box<dyn Animator>,
}
impl Entity {
pub fn new(
pos: Pos,
shape: Box<dyn Shape>,
animator: Box<dyn Animator>,
) -> Self {
Self {
pos,
shape,
animator,
}
}
pub fn pos(&self) -> &Pos {
&self.pos
}
pub fn pos_mut(&mut self) -> &mut Pos {
&mut self.pos
}
pub fn change_color(
&mut self,
color: String,
) {
self.shape.change_color(color);
}
pub fn draw(&self) {
self.shape.draw(self);
}
pub fn animate(&mut self) {
let anim = &mut self.animator;
anim.animate(self);
}
}
}
mod custom {
use super::common::{Animator, Entity, Shape};
pub struct MyShape {
color: String,
}
impl MyShape {
pub fn new(color: String) -> Self {
Self { color }
}
}
impl Shape for MyShape {
fn draw(
&self,
entity: &Entity,
) {
println!("draw at {:?} with {:?}", entity.pos(), self.color);
}
fn change_color(
&mut self,
color: String,
) {
self.color = color;
}
}
pub struct MyAnim {
count: i32,
}
impl MyAnim {
pub fn new() -> Self {
Self { count: 0 }
}
}
impl Animator for MyAnim {
fn animate(
&mut self,
entity: &mut Entity,
) {
let pos = entity.pos_mut();
if (self.count % 2) == 0 {
pos.x += 0.1;
pos.y += 0.2;
} else {
pos.x += 0.2;
pos.y += 0.1;
}
self.count += 1;
if self.count >= 3 {
entity.change_color("red".to_owned());
}
}
}
}
fn main() {
use common::{Entity, Pos};
use custom::{MyAnim, MyShape};
let mut entity = Entity::new(
Pos { x: 0.0, y: 0.0 },
Box::new(MyShape::new("green".to_owned())),
Box::new(MyAnim::new()),
);
entity.draw();
for _ in 0..5 {
entity.animate();
entity.draw();
}
}
如您所见,所提供的代码无法编译,因为在第66行,anim
是对负责动态分派的Animator
组件的可变引用,但是该方法的参数也是对作为整体的Entity
的可变引用,该Entity
包含先前的Animator
。如果我们希望Animator
能够对实体进行修改,就需要这个参数。我陷入了这种情况,只能考虑一些看起来很糟糕的变通方法:
- 不要将实体作为参数传递,而是将其每个字段(动画制作者除外)作为多个参数传递:那么定义struct的意义何在呢?(如果一个实体由12个字段组成,那么每次操作该实体时,是否应该传递11个参数?)
- 在
RefCell
中嵌入实体每个字段,并假设每个函数的每个参数都是非可变引用,那么borrow_mut()
将出现在任何我们想要的地方,并希望它不会出现异常:对我来说,这就像放弃了函数原型告诉并执行代码的 * 意图 * 的想法(让我们到处添加一些Rc
,以便完全忘记谁拥有什么,我们获得了Java;^)
我确信我做了一些错误的选择,哪些应该是独占的(&mut
),哪些应该是共享的(&
),但我看不到一个合理的界限。在我看来,当一个实体必须被动画化时,它自己需要关注:除了 * 查看 * 周围环境的状态(但不改变它)之外,没有什么可以共享的。如果我们共享所有内容,并依赖内部可变性来实现运行时的安全突变(由于引用计数),我听起来像是:让我们疯狂地开车,就好像没有交通规则一样,只要没有人抱怨(try_borrow()
/try_borrow_mut()
),我们没有任何事故(panic!()
)。
为了实现预期的行为,有人能建议我的结构/函数的更好的组织吗?由一些动态(如OOP)组件组成的实体,负责相关实体上的操作细节?
1条答案
按热度按时间c9qzyr3d1#
许多个月后...我回答了我自己的问题,以防有人对我决定使用的解决方案发表评论。
作为第一次尝试,正如@cdhowie善意地建议的那样,我首先将
Entity
的数据成员(这里只有pos
)隔离在EntityState
结构中,该结构用作Entity
的唯一数据成员,这样,我可以使Animator::animate()
期望state: &mut EntityState
而不是entity: &mut Entity
作为参数;这样做,Animator
的一个实现能够改变Entity
的位置。然而,我并不完全满意,因为这导致Entity
的一些成员之间的严格区别,仅仅是因为借用检查器。例如,我无法从Animator
调用Entity::change_color()
,因为它暗示EntityState
中没有shape
成员。当然,我们也可以决定在EntityState
中包含shape
,但是,如果我们有另一个行为成分(Interactor
......)能够使Entity
(它的state
)发生突变,并受到其他行为成分的突变(如Animator
可能想要使Shape
发生突变),那会怎么样呢?我发现很难定义一个一般规则,以决定哪些成员应该站在EntityState
或只是在Entity
(并使用内部-每个成员的可变性在我看来很麻烦)。偶然的是,在努力解决回调问题时(实际上,它们与这个问题非常相似),我发现了this answer,它使用了一个 * 技巧 *,一旦别人比我发明了它,我就发现它是聪明而明显的!调用时需要
&mut self
的行为成员存储在Option
中。它只是在调用之前从Option
中取出,然后放回:这样调用的&mut Entity
参数就不能再通过Entity
到达它,并且借用检查器发现这种情况是正确的。这个解决方案只需要对代码的原始组织做最小的修改,并且,就我所能预见的,当场景变得更复杂时(更多的行为组件,最终交互),它似乎仍然可用。回到问题中提供的示例,只需要做三个小的更改(playground)。在结构中,成员被 Package 到
Option
中。显然,这样的结构的构造考虑了这个
Option
。要点在于:从
Option
中获取行为成员,调用其函数,提供对整个结构的引用,将行为成员放回Option
中。