【问题标题】:How to use composition instead of inheritance? Enum_dispatch? [closed]如何使用组合而不是继承?枚举调度? [关闭]
【发布时间】:2021-05-09 05:59:56
【问题描述】:

我知道我应该使用组合而不是继承。

在 Java/C++ 中,我有一个抽象基类 Vehicle(带有属性和常用方法)和实现它的类,例如 CarBike

我找到了enum_dispatch,它在将方法调用从父“类”转发到子“类”方面做得很好。

#[enum_dispatch]
pub trait VehicleDelegation {
    fn drive(&mut self) -> anyhow::Result<()>;
}

#[enum_dispatch(VehicleDelegation)]
pub enum Vehicle {
    Car,
    Bike,
}

pub struct Car {
    pub doors: u8
}

impl VehicleDelegation for Car {
    fn drive(&mut self) -> anyhow::Result<()> {
        // do car stuff
        Ok(())
    }
}

pub struct Bike {
    pub frame_size: u8
}

impl VehicleDelegation for Bike {
    fn drive(&mut self) -> anyhow::Result<()> {
        // do bike stuff
        Ok(())
    }
}
  • 我应该在哪里存储公共数据,例如 num_wheels,它本来是 Vehicle 的属性?
  • 我应该在哪里定义Vehicle使用公共数据的方法?

目前我必须将所有 Vehicle 数据和方法复制到每个枚举变体中,这变得越来越烦人,因为它会随着“方法”的数量和“类”的数量而增加。

  • 如何在 Rust 中以惯用方式做到这一点?

【问题讨论】:

  • 你可以有一个结构,其中一个字段是你上面的Vehicle枚举;公共数据的其他字段,并为公共方法在该结构上实现实现。
  • @eggyal 谢谢,我考虑过这一点,但是Vehicle 枚举变体如何访问它们共同的父级数据?这需要为 mut 工作,因此在枚举变体中保留对父级的引用是行不通的。
  • 我投票“需要详细信息”,但真正的问题是Vehicle/Bike 等只是一个玩具示例;它是纯粹的结构。它不能解决真正的问题,因此不可能就如何改进它提出明智的建议。 您不能将程序的设计与其解决的问题分开。 像这样的玩具示例的主要作用是演示继承如何工作,而这根本不可能在 Rust 中完成,因为 Rust 没有继承。
  • 如果您在设计一些解决问题的程序时遇到困难,而不仅仅是从 Java 书籍或任何演示继承的玩具程序,仅此而已,为了回答这个问题,我们需要通知设计的要求和约束。其他一切都是浪费时间。请记住,大多数微小的、琐碎的程序都不够复杂,不需要像继承这样的复杂特性;需求源于复杂性,因此架构问题的minimal reproducible example 通常比语法错误要大得多。
  • @trentcl 谢谢,我同意你的观点,但没有明显的解决方案。我问了一个玩具问题,因为我认为发布数百行特定于域的代码会使问题变得难以理解。 (我想我已经想出了一个可行的解决方案,我打算将它与一个“玩具”插图一起发布,如果它能够在几天的开发中存活下来。)

标签: inheritance rust enums dispatch enum-dispatch


【解决方案1】:

enum_dispatch 是一个实现非常具体的优化的 crate,即当 trait 实现的数量很少且事先已知时,避免基于 vtable 的动态调度。在了解特征如何正常工作之前,可能不应该使用它。

在 Rust 中,您从一个 trait 开始,例如 Vehicle,它大致相当于 Java 接口:

pub trait Vehicle {
    fn drive(&mut self) -> anyhow::Result<()>;
}

你可以在任何你想要的类型上实现这个特性。如果他们需要一个通用的“基本类型”,您可以创建一个:

// Car and Bike can but don't have to actually use BaseVehicle in their
// implementations. As long as they implement the Vehicle trait, they're fine.

struct BaseVehicle {
    num_wheels: u8,
}

struct Car {
    base_vehicle: BaseVehicle,
    doors: u8,
}

impl Vehicle for Car {
    fn drive(&mut self) -> anyhow::Result<()> {
        // do car stuff
        Ok(())
    }
}

struct Bike {
    base_vehicle: BaseVehicle,
    frame_size: u8,
}

impl Vehicle for Bike {
    fn drive(&mut self) -> anyhow::Result<()> {
        // do bike stuff
        Ok(())
    }
}

当一个函数需要接受任何车辆时,它会接受&amp;mut dyn VehicleBox&lt;dyn Vehicle&gt;(取决于它是需要借用还是接管车辆的所有权)。

回答您的问题:

  • 我应该在哪里存储常用数据,例如 num_wheels 这本来是 Vehicle 的属性?

只要它适合您的程序。如上所示,典型的方法是将它们放在所有车辆都包含通过组合的“基础”或“通用”类型中。例如,num_wheels 可用作 self.common_data.num_wheels

  • 我应该在哪里定义使用公共数据的 Vehicle 方法?

同样,它们将被定义在公共类型上,它们可以被特定类型访问,并且(如果需要)通过它们的 trait 实现公开。

如果“基础”类型足够丰富,它本身就可以实现特征——在这种情况下,这意味着提供一个impl Vehicle for BaseVehicle。然后,具体类型的Vehicle 实现可以将它们的方法实现转发给self.base_vehicle.method(),这相当于super() 调用。这种方法增加了很多样板文件,所以我不会推荐它,除非它真的有意义,即除非BaseVehicle 实际上提供了Vehicle 的连贯且有用的实现。

【讨论】:

  • 我从dyn转到enum,因为enum完美地满足了复杂的serde要求。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-02-17
  • 1970-01-01
  • 2023-02-24
  • 2010-10-19
相关资源
最近更新 更多