Rust中的OOP和共享/可变引用

mspsb9vt  于 2023-03-02  发布在  其他
关注(0)|答案(1)|浏览(160)

在一系列编程课程的背景下,我决定使用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)组件组成的实体,负责相关实体上的操作细节?

c9qzyr3d

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中。

pub struct Entity {
        pos: Pos,
        shape: Box<dyn Shape>,
        // animator: Box<dyn Animator>, // an option is required
        animator: Option<Box<dyn Animator>>,
    }

显然,这样的结构的构造考虑了这个Option

impl Entity {
        pub fn new(
            pos: Pos,
            shape: Box<dyn Shape>,
            animator: Box<dyn Animator>,
        ) -> Self {
            Self {
                pos,
                shape,
                // animator, // make use of the Option
                animator: Some(animator),
            }
        }
// ...

要点在于:从Option中获取行为成员,调用其函数,提供对整个结构的引用,将行为成员放回Option中。

// ...
        pub fn animate(&mut self) {
            // let anim = &mut self.animator; // take-from/put-back
            // anim.animate(self);
            if let Some(mut anim) = self.animator.take() {
                anim.animate(self);
                self.animator = Some(anim);
            }
        }
    }

相关问题