一般的答案是使用组合而不是继承。根据应用程序的不同,组合应该采用不同的方式。在大多数情况下,您应该从
struct VehicleState {
lat: f64,
lon: f64,
alt: f64,
speed: f64,
}
剩下的问题就是如何使用不同类型的车辆。
方式 1:如果代码的不同部分以不同的、不重叠的方式使用不同类型的车辆,您可以简单地在特定结构中包含状态结构:
struct Cab {
state: VehicleState,
// ... other fields
}
struct PackerMover {
state: VehicleState,
// ... other fields
}
这是最直接类似于 C++ 继承的版本,特别是在内存布局和静态类型方面。然而,这使得访问不同车辆的公共 state 变得很尴尬,并且它不支持动态调度,除非你编写一个带有访问 state 方法的特征(这对你的代码类型有一些限制)可以写)。通常应避免使用这种方法,除非您知道不需要其他任何东西。
方式 2:如果有代码应该对正在使用的车辆类型通用,但这是静态决定的,您可以创建一个通用结构:
struct Vehicle<T> {
state: VehicleState,
details: T,
}
struct Cab { /* ... */ }
struct PackerMover { /* ... */ }
/// This function only works with Cabs
fn foo(vehicle: Vehicle<Cab>) { /* ... */ }
/// This function works with any Vehicle
fn foo<T>(vehicle: Vehicle<T>) { /* ... */ }
这使得访问 state 变得容易,并且所有使用都是静态调度的。
如果你对 Vehicle 做一个小改动并添加一个特征,它也可以被动态调度:
struct Vehicle<T: ?Sized> { /* ... */
// ^^^^^^^^ remove default restriction on the type parameter
trait VehicleDetails { /* add methods here */ }
impl VehicleDetails for Cab { /* ... */ }
impl VehicleDetails for PackerMover { /* ... */ }
这允许您将引用(或指针或Box)&Vehicle<Cab>强制转换为&Vehicle<dyn VehicleDetails>,这是一个指针指向的类型任何Vehicle谁的T实现了VehicleDetails。例如,这可用于将各种车辆放入Vec<Box<Vehicle<dyn VehicleDetails>>>。使用 dyn 会导致通过 vtables 进行分派,就像 C++ 虚拟方法一样。
(Info on this language feature。文档说“自定义 DST 目前基本上是一个半生不熟的功能”,但这种特殊情况正是它们的情况做工作没有任何问题。)
如果您希望能够找出正在使用哪个“子类”并专门与之交互,这不是一个好的选择;如果车辆的所有特定特性都可以在 VehicleDetails 特性中表达,这是一个不错的选择。
方式 3:如果应用程序将要例行公事使用动态选择的车辆类型——特别是如果它经常想问“这辆车是Cab”然后与其Cabness 交互——那么你应该使用enum来包含详细信息。
struct Vehicle {
state: VehicleState,
kind: VehicleKind,
}
enum VehicleKind {
Cab {
seats: u16,
},
PackerMover {
cargo_capacity: u64,
}
}
这是动态调度,因为每个 Vehicle 都可以是任何类型,因此您始终可以混合和匹配车辆类型,但不涉及任何指针或 vtable。主要缺点是将其扩展到新种类需要修改单个enum VehicleKind,因此这不适合用户将使用 C++ 编写子类的库。但是,与我上面提到的 Vehicle<dyn VehicleDetails> 相比,使用它要简单得多。