我应该使用枚举还是装箱的 trait 对象来模拟多态性?

23
使用枚举类型 Axes 来限制 CoordinateQuaternion
#[derive(Clone)]
pub enum Axes {
    Coordinate {
        x: f64,
        y: f64,
        z: f64,
        reserve: Vec<f64>,
    },
    Quaternion {
        x: f64,
        y: f64,
        z: f64,
    },
}

impl Axes {
    pub fn shift(&mut self, Sample: &Axes) -> () {
        let Dup: Axes = self.clone();
        match Dup {
            Axes::Coordinate { x, y, z, reserve } => match &Sample {
                Axes::Coordinate { x, y, z, reserve } => {
                    *self = Axes::Coordinate {
                        x: *x,
                        y: *y,
                        z: *z,
                        reserve: reserve.to_vec(),
                    };
                }
                _ => panic!(),
            },
            Axes::Quaternion { x, y, z } => match &Sample {
                Axes::Quaternion { x, y, z } => {
                    *self = Axes::Quaternion {
                        x: *x,
                        y: *y,
                        z: *z,
                    };
                }
                _ => panic!(),
            },
        }
    }
}

使用特质 AxesCoordinateQuaternion 结构体进行链接:

pub trait Axes {
    fn shift(&mut self, Sample: &Axes) -> ();
    fn fold(&mut self, Sample: &Axes) -> ();
}

pub struct Coordinate {
    pub x: f64,
    pub y: f64,
    pub z: f64,
    pub reserve: Vec<f64>,
}

pub struct Quaternion {
    pub x: f64,
    pub y: f64,
    pub z: f64,
}

impl Axes for Coordinate {
    fn shift(&mut self, Sample: &Axes) -> () {}
    fn fold(&mut self, Sample: &Axes) -> () {}
}

impl Axes for Quaternion {
    fn shift(&mut self, Sample: &Axes) -> () {}
    fn fold(&mut self, Sample: &Axes) -> () {}
}

在这种情况下,结构体上实现的特质更易访问和更高效吗?我不确定在什么情况下使用哪个。


与您的问题无关,但在Rust中,函数、字段和变量几乎总是小写的。如果您以这种方式大写事物,其他程序员可能会困惑阅读您的代码。当然,只有在您计划让其他程序员阅读您的代码时才会出现这个问题。 :) - Kwarrtz
在这个主题中找到有趣的观点这里 :) - iago-lito
2个回答

19
使用特征(trait)和枚举(enum)的一个重要区别在于它们的可扩展性。如果你将 Axes 定义为枚举,那么这两个选项会被硬编码到类型中。如果你想添加第三种轴形式,就需要修改类型本身,这可能会涉及到使用 Axes 的代码的大量修改(例如任何匹配 Axes 的代码都需要被修改)。另一方面,如果你将 Axes 定义为特征(trait),你可以通过定义新类型并编写适当的实现来添加其他类型的轴,而无需修改现有的任何代码。甚至可以由用户在库的外部完成这个过程。
另一个需要考虑的重要因素是对结构体内部数据的访问权限。对于枚举,您可以完全访问存储在结构体内部的所有数据。如果您想编写一个可以使用 trait 在 CoordinateQuaternion 上操作的函数,那么你只能执行在 Axes trait (在此例中包括 ShiftFold)中描述的操作。例如,根据您提供的 Axes 实现,您无法通过 Axes 接口简单地检索 (X,Y,Z) 元组。如果您在某个时候需要这样做,就必须添加一个新方法。

如果不知道您计划如何使用这些类型,很难确定哪个选项更好,但如果是我,我可能会使用枚举。最终,这很大程度上取决于个人喜好,但希望这能让您在做出决定时思考一些事情。


正如@Kwarrtz所说,首先,特质更加灵活,并且不会违反开闭原则;其次,特质的分派是自动化的,而枚举需要自己处理。 - superK

17

在 @Kwarrtz 的答案中没有提到的另一个区别与内存相关。

  • enum 可以直接存储在堆栈上,而装箱的 trait 总是需要堆。也就是说,enum 创建起来很便宜,但装箱的 trait 不是。
  • 即使你大多数存储的都是小变量,enum 实例的大小始终等于其最大变体的大小(在大多数情况下还会有一个鉴别器)。这在如下情况下可能会成为问题:

    enum Foo {
        SmallVariant(bool),
        BigVariant([u64; 100]),
    }
    

    如果您在向量中存储了 N 个此类型的实例,则该向量始终需要 N*(100*sizeof::<u64> + sizeOfDiscriminant) 字节内存,即使向量只包含 SmallVariants。

    如果您使用的是 boxed trait,则向量将使用 N * sizeOfFatPointer == N * 2 * sizeof::<usize>


6
为了完整起见,如果你只将数组放在 BigVariant 中,那么向量的大小最终与封装特性版本相同,但除了堆栈分配之外,你仍然可以获得 enum 的其他优点。 - trent
如果一个枚举存储在堆栈中,如果我放置类似于String?的东西会发生什么? - mamcx
@mamcx,String 本身总是必须在堆上分配其内部缓冲区,但将其放入 enum 中不会进一步包装它,而 Box<SomeTraitImplementedByString> 将需要两次间接寻址。 - mcarton

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接