只比较枚举类型的变量而不是值

82

我有一个枚举类型,其结构如下:

enum Expression {
    Add(Add),
    Mul(Mul),
    Var(Var),
    Coeff(Coeff)
}

每个变体的“成员”都是结构体。

现在我想比较两个枚举是否具有相同的变体。因此,如果我有:

let a = Expression::Add({something});
let b = Expression::Add({somethingelse});

cmpvariant(a, b) 应该返回 true。我可以想象一个简单的双重匹配代码,遍历两个枚举实例的所有选项。但是,如果存在更复杂的解决方案,我正在寻找它。如果没有,双重匹配是否会有额外开销?我想在内部只是比较两个整数(最理想的情况)。


1
不是关于你的问题,但是你能告诉我 Add(Add) 语法的含义吗?第一个 Add 是什么,第二个又是什么? - Martin Thoma
3
@moose: 第一个 Add 是枚举变量的名称。第二个是该变量的类型,它假定存在另一个类型(一个 struct 或另一个 enum,或者可能是一个类型别名)名为 Add,其定义未在此处显示。请注意,变量的名称不需要与其类型的名称相同,这只是 OP 选择的命名方式。 - Benjamin Lindley
1个回答

98

从Rust 1.21.0版本开始,你可以使用std::mem::discriminant函数:

fn variant_eq(a: &Op, b: &Op) -> bool {
    std::mem::discriminant(a) == std::mem::discriminant(b)
}

这很好,因为它可以非常通用:

fn variant_eq<T>(a: &T, b: &T) -> bool {
    std::mem::discriminant(a) == std::mem::discriminant(b)
}

在 Rust 1.21.0 之前,我会匹配两个参数的元组,并使用 _.. 忽略元组的内容:

struct Add(u8);
struct Sub(u8);

enum Op {
    Add(Add),
    Sub(Sub),
}

fn variant_eq(a: &Op, b: &Op) -> bool {
    match (a, b) {
        (&Op::Add(..), &Op::Add(..)) => true,
        (&Op::Sub(..), &Op::Sub(..)) => true,
        _ => false,
    }
}

fn main() {
    let a = Op::Add(Add(42));
    
    let b = Op::Add(Add(42));
    let c = Op::Add(Add(21));
    let d = Op::Sub(Sub(42));

    println!("{}", variant_eq(&a, &b));
    println!("{}", variant_eq(&a, &c));
    println!("{}", variant_eq(&a, &d));
}

虽然组成枚举类型的部分被称为变体,实际上你正在测试它们是否相等,而不是比较它们(这通常用于排序)。因此,我冒昧地给函数改了个名字。

就性能而言,让我们看看在Rust 1.60.0中,在发布模式下生成的LLVM IR(并将variant_eq标记为#[inline(never)])。你可以在Rust Playground中查看:

; playground::variant_eq
; Function Attrs: mustprogress nofree noinline norecurse nosync nounwind nonlazybind readonly uwtable willreturn
define internal fastcc noundef zeroext i1 @_ZN10playground10variant_eq17hc64d59c7864eb861E(i8 %a.0.0.val, i8 %b.0.0.val) unnamed_addr #2 {
start:
  %_8.not = icmp eq i8 %a.0.0.val, %b.0.0.val
  ret i1 %_8.not
}

这段代码直接比较变量的判别式。

如果你想要一个生成该函数的宏,可以尝试类似下面的代码作为起点。

struct Add(u8);
struct Sub(u8);

macro_rules! foo {
        (enum $name:ident {
            $($vname:ident($inner:ty),)*
        }) => {
            enum $name {
                 $($vname($inner),)*
            }

            impl $name {
                fn variant_eq(&self, b: &Self) -> bool {
                    match (self, b) {
                        $((&$name::$vname(..), &$name::$vname(..)) => true,)*
                        _ => false,
                    }
                }
            }
        }
    }

foo! {
    enum Op {
        Add(Add),
        Sub(Sub),
    }
}

fn main() {
    let a = Op::Add(Add(42));

    let b = Op::Add(Add(42));
    let c = Op::Add(Add(21));
    let d = Op::Sub(Sub(42));

    println!("{}", Op::variant_eq(&a, &b));
    println!("{}", Op::variant_eq(&a, &c));
    println!("{}", Op::variant_eq(&a, &d));
}

但是,宏确实存在一些限制 - 所有变体都需要具有单个变体。支持单位变体、具有多种类型的变体、结构变体、可见性等都非常困难。也许过程宏会使它稍微容易一些。


谢谢!我会编辑我的原始帖子以反映正确的语言。您有双重匹配的开销的想法吗? - Ben Ruijl
1
discriminant_value 的存在是为了做到这一点,尽管它是内部函数,但它是不稳定的。当生成 deriving 代码时,至少会使用它,因此自动派生的 ==、< 等运算符至少能够达到最快的速度。 - Mar
9
还是很丑陋。我希望有类似于 some_variant == SomeEnum::Variant(_) 这样的语法。但现在,我不得不写许多辅助函数来实现同样的效果。 - Evgeni Nabokov
2
@RecursiveExceptionException不是OP的目标。他们有两个枚举实例,希望将它们相互比较。您的代码解决了“这个实例是否是特定变体”的问题。这已经在如何有条件地检查枚举是否为一种变体或另一种变体?中涵盖了。 - Shepmaster
2
这已经被优化了,现在产生的汇编输出更加简洁:https://godbolt.org/z/5s3xcrcoK - Kuba Beránek
显示剩余3条评论

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