当枚举类型一致时,如何匹配枚举对?

3

我希望消除这里重复的代码,而不必列出每个枚举种类:

use Geometry::*;
let geometry = match (&self.geometry, &other.geometry) {
    (Point(a), Point(b)) => Point(*a.interpolate(b, t)),
    (Curve(a), Curve(b)) => Curve(*a.interpolate(b, t)),
    (EPBox(a), EPBox(b)) => EPBox(*a.interpolate(b, t)),
    (_, _) => unimplemented!("Kinds not matching")
};

种类中的类型都实现了Interpolate特质,该特质拥有interpolate方法:
enum Geometry {
    Point(Point),
    Curve(Curve),
    EPBox(EPBox)
}

trait Interpolate {
    fn interpolate(&self, other: &Self, t: f64) -> Box<Self> {
        // ...
    }
}

impl Interpolate for Point {}
impl Interpolate for Curve {}
impl Interpolate for EPBox {}

我想做的是类似于这样的事情:
let geometry = match (&self.geometry, &other.geometry) {
    (x(a), x(b)) => x(*a.interpolate(b, t)), // and check that a and b impls Interpolate
    (_, _) => unimplemented!("Kinds not matching")
};

这可以通过枚举定义上的过程宏来完成。 - Chayim Friedman
1
注意:创建一个 Box<Self>,然后解包它 (*a) 似乎是多余的。 - battlmonstr
谢谢@ChayimFriedman,我会研究过程宏。你是对的@battlmonstr,将interpolate的返回类型切换为Self where Self: Sized可以解决问题并消除Box<Self>。但我的IDE说我需要使用Box... - Jonatan Kallus
3个回答

4

我不确定您要实现多少个这样的操作以及您实际上有多少枚举变量。根据这些情况,最佳答案会有很大变化。如果您只有一个操作(interpolate)和三种变量:请将其输入,其他任何方式都会更加复杂和难以维护。

话虽如此,我可能会使用:

let geometry = binop!(match (&self.geometry, &other.geometry) {
    (a, b) => *a.interpolate(b, t)
});

基于

macro_rules! binop {
    (match ($av:expr, $bv:expr) { ($a:ident, $b:ident) => $op:expr }) => {{
        use Geometry::*;
        binop!($av, $bv, $a, $b, $op, Point, Curve, EPBox)
    }};
    ($av:expr, $bv:expr, $a:ident, $b:ident, $op:expr, $($var:path),*) => {
        match ($av, $bv) {
            $(($var($a), $var($b)) => $var($op),)*
            _ => unimplemented!("Kinds not matching")
        }
    };
}

Playground

您需要列出枚举变量。如果不想这样做,您可以选择使用过程宏。我认为这样做有点过头了。


只会有一个操作(interpolate),但会有许多(10-100)个枚举变量。 - Jonatan Kallus
2
100?哦,好吧,也许你确实需要一个过程宏。(要么这样,要么你有一些普通的宏生成你的枚举和大量的匹配语句。) - Caesar

1

根据您当前的代码,您无法真正做到这一点。使用core::mem::discriminant检查枚举变量是否为相同变量相当简单,但是您无法在没有matchif let语句的情况下访问包含的值。您可以使用类型参数来处理变量和类型ID。这是一种非常糟糕的方法,应该尽量避免使用,但它确实有效。我已经给出了一个示例,以向您展示如何实现它。

use std::any::Any;

trait SomeTrait {
    fn some_function(&self, other: Self) -> &Self;
}

#[derive(Clone)]
struct Var0 {
    eg: i64
}

#[derive(Clone)]
struct Var1 {
    other_eg: f64
}

fn check<T: 'static, B: 'static>(lhs: T, rhs: B) -> Option<T> {
    if lhs.type_id() == rhs.type_id() {
        Some(lhs.some_function(rhs))
    } else {
        None
    }
}

fn main() {
    let a = Var0{ eg: 2 };
    let b = Var0{ eg: 9 };
    let c = Var1 { other_eg: -3f64 };
    assert!(check(a.clone(), b).is_some());
    assert!(check(a, c).is_none());
}

impl SomeTrait for Var0 { /* ... */ }
impl SomeTrait for Var1 { /* ... */ }

请注意:正如我之前所说的那样,这很混乱、相当hacky并且不易于阅读。如果是我写的话,我会使用match语句替换它,但你可以自行决定采取什么方式来实现,因为需要对其进行一些修改。我也不确定是否可以为check函数使用不同于`static的生命周期,这可能会成为一个问题。

这个答案与上面关于使用过程宏的评论相矛盾。您不认为我的问题可以通过使用过程宏来解决吗? - Jonatan Kallus
1
这真的不能编译通过,对吧?Playground 问题在于 check 必须要求 T = B 才能调用 some_function,但如果这样做,它就变得无用了。SomeTrait(以及 Interpolate)不是对象安全的,因此您也无法使用动态分派来解决此问题。我有什么遗漏的地方吗? - Caesar
我没有太多使用宏的经验,所以无法对其使用发表评论。而且我很确定我是在本地编译的,但不记得具体怎么做了,抱歉。 - nxe

0

我想出了以下解决方案,可以轻松扩展新的插值类型:

macro_rules! geometries {
    ( $( $x:ident ),+ ) => {
        enum Geometry {
        $(
            $x($x),
        )*
        }
        fn interpolate_geometries(a: &Geometry, b: &Geometry, t: f64) -> Geometry {
            use Geometry::*;
            match (a, b) {
            $(
                ($x(a), $x(b)) => $x(a.interpolate(b, t)),
            )*
                _ => unimplemented!("Kinds not matching")
            }
        }
        $(
        impl Interpolate for $x {}
        )*
    };
}

geometries!(Point, Curve, EPBox);

这使得以后可以做到:

let geometry = interpolate_geometries(&self.geometry, &other.geometry, t);

感谢所有的回答和评论!特别是那个 binop 宏,它与我的解决方案非常相似。在阅读了这里的评论后,我想出了这个解决方案。


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