如何将枚举值与整数匹配?

99

我可以这样获取枚举类型的整数值:

enum MyEnum {
    A = 1,
    B,
    C,
}

let x = MyEnum::C as i32;

但是我似乎做不到这个:
match x {
    MyEnum::A => {}
    MyEnum::B => {}
    MyEnum::C => {}
    _ => {}
}

我该如何匹配枚举的值或者尝试将 x 转换回 MyEnum
我可以看到对于枚举类型,像下面这样的函数会非常有用,但实际上它并不存在:
impl MyEnum {
    fn from<T>(val: &T) -> Option<MyEnum>;
}

10
你可能希望解释为什么想这样做。我建议你保持枚举类型,并且可以添加一个解析步骤,如果真的需要,就可以将其转换回来。 - Shepmaster
14
C错误代码:我调用一个可能返回整数类型错误的C函数。我希望我的错误代码是兼容的。当然,我可以使用枚举分别处理每个错误,但这需要额外的代码。 - dhardy
重新考虑后,将 MyEnum 改为 enum MyEnum { A = 1, B, C, Other(i32) } 可能是解决我的问题的更好方案。但我会保留原始问题供其他用户参考(我以前肯定也想过这样做,不知道是出于坏习惯还是缺乏好的替代方案)。 - dhardy
1
将其转换为类似于 enum Codes { A = 1, B, C }; enum MyEnum { Code(Codes), Other(i32) }; 的形式。 - dhardy
@shepmaster 我也是,我正在解码指令。我自豪地将一个nibble的含义键入了枚举中(认为“我是个好的rust人,不用#define”),结果被告知不行。我想我只能使用一组常数了。 - pm100
10个回答

77
自 Rust 1.34 起,我建议实现 TryFrom
use std::convert::TryFrom;

impl TryFrom<i32> for MyEnum {
    type Error = ();

    fn try_from(v: i32) -> Result<Self, Self::Error> {
        match v {
            x if x == MyEnum::A as i32 => Ok(MyEnum::A),
            x if x == MyEnum::B as i32 => Ok(MyEnum::B),
            x if x == MyEnum::C as i32 => Ok(MyEnum::C),
            _ => Err(()),
        }
    }
}

然后你可以使用TryInto并处理可能的错误:

use std::convert::TryInto;

fn main() {
    let x = MyEnum::C as i32;

    match x.try_into() {
        Ok(MyEnum::A) => println!("a"),
        Ok(MyEnum::B) => println!("b"),
        Ok(MyEnum::C) => println!("c"),
        Err(_) => eprintln!("unknown number"),
    }
}

如果您有大量的变体,可以使用宏根据枚举定义自动创建TryFrom的并行实现。
macro_rules! back_to_enum {
    ($(#[$meta:meta])* $vis:vis enum $name:ident {
        $($(#[$vmeta:meta])* $vname:ident $(= $val:expr)?,)*
    }) => {
        $(#[$meta])*
        $vis enum $name {
            $($(#[$vmeta])* $vname $(= $val)?,)*
        }

        impl std::convert::TryFrom<i32> for $name {
            type Error = ();

            fn try_from(v: i32) -> Result<Self, Self::Error> {
                match v {
                    $(x if x == $name::$vname as i32 => Ok($name::$vname),)*
                    _ => Err(()),
                }
            }
        }
    }
}

back_to_enum! {
    enum MyEnum {
        A = 1,
        B,
        C,
    }
}

另请参阅:


1
我无法理解,看起来像是魔法。x 的类型是 i32 对吧?那么 Rust 是如何知道它必须与 MyEnum(而不是实现了该 trait 的其他枚举)的变体匹配呢? - Nurbol Alpysbayev
3
@NurbolAlpysbayev,必须使用与 MyEnum 对应的 TryInto 实现,因为在匹配分支中使用了该值的类型 (Ok(MyEnum::A))。Rust 如何从 From::<>::from() 推断结果类型?; Rust 的类型推断如何跨多个语句工作? - Shepmaster
我会阅读这些链接,但简单来说,Rust会查看第一个分支并确保接下来的分支与相同的枚举类型(然后还确保完整性),我是对的吗?看起来是这种情况。 - Nurbol Alpysbayev
不幸的是,我发现这个解决方案可能存在一个缺点,即在try_from函数中没有检查穷尽性。因此基本上它是不可靠的。 - Nurbol Alpysbayev
@NurbolAlpysbayev,你似乎在使用多个(非标准?)含义的单词“exhaustive”。你可能会对如何确保每个枚举变量都可以在编译时从特定函数返回?感兴趣。 - Shepmaster
显示剩余6条评论

68
你可以使用Rust 2018简化的导入语法来派生FromPrimitive
use num_derive::FromPrimitive;    
use num_traits::FromPrimitive;

#[derive(FromPrimitive)]
enum MyEnum {
    A = 1,
    B,
    C,
}

fn main() {
    let x = 2;

    match FromPrimitive::from_i32(x) {
        Some(MyEnum::A) => println!("Got A"),
        Some(MyEnum::B) => println!("Got B"),
        Some(MyEnum::C) => println!("Got C"),
        None            => println!("Couldn't convert {}", x),
    }
}

在你的 Cargo.toml 文件中:
[dependencies]
num-traits = "0.2"
num-derive = "0.2"

更多细节请查看num-derive crate,尤其是查看用于测试的示例使用


43

您可以利用匹配守卫来编写等效但更加繁琐的构造:

match x {
    x if x == MyEnum::A as i32 => ...,
    x if x == MyEnum::B as i32 => ...,
    x if x == MyEnum::C as i32 => ...,
    _ => ...
}

std::mem::transmute 也可以使用:

let y: MyEnum = unsafe { transmute(x as i8) };

但这需要您知道枚举的大小,以便首先将其转换为适当的标量,并且如果x不是枚举的有效值,则还会产生未定义的行为。


2
Match-guards是一个不错的最后手段。但是,我真的不想在内存中假设枚举的表示方式——它很可能会在某个新的Rust版本中出现问题。避免使用不安全的代码是使用Rust的最大原因! - dhardy
1
我会把这个操作封装成 MyEnum 的一个方法。然后你可以调用 MyEnum::from_integer 或其他方法。 - Shepmaster
假设你只有一个枚举变量列表,并在定义中分配了值,那么这个方法是我支持的。如果你一开始就将数字转换为枚举类型,那么没有逻辑上的理由不可以再次映射回去。这也意味着你可以迭代简单的枚举类型而无需使用外部包。 - S.W.

34

如果您确信枚举类型包含整数的值,可以使用std::mem::transmute

应与#[repr(..)]一起使用,以控制底层类型。

完整示例:

#[repr(i32)]
enum MyEnum {
    A = 1, B, C
}

fn main() {
    let x = MyEnum::C;
    let y = x as i32;
    let z: MyEnum = unsafe { ::std::mem::transmute(y) };

    // match the enum that came from an int
    match z {
        MyEnum::A => { println!("Found A"); }
        MyEnum::B => { println!("Found B"); }
        MyEnum::C => { println!("Found C"); }
    }
}

请注意,与其他答案不同,这只需要Rust标准库。


2
这个回答已经提到了 transmute,并且只需要使用标准库。似乎这个回答应该只是对那个回答的评论,建议使用 repr - Shepmaster
1
@Shepmaster,我考虑过编辑或建议编辑其他答案。但我认为这会使它成为另一个答案。无论如何,如果这个答案被复制粘贴到另一个答案中,我并不介意,只是这样做似乎有点奇怪。 - ideasman42
6
使用一个不安全的函数去完成这么简单的事情看起来像是一种非常糟糕的做法... - Sean Burton
1
@sean-burton - 我同意你不想让你的代码充满了不安全的转换,但是如果你要与C库进行接口交互,除了编写一些繁琐、需要保持同步并可能引入人为错误的查找表之外,你并没有其他选择。因此,在我看来,这是两害相权取其轻的做法。 - ideasman42
1
@SeanBurton 编写一个基本上是将 1:1、2:2、3:3 进行映射的 switch 似乎也不是很好的实践。 - c z

17

std::num::FromPrimitive被标记为不稳定的,不会包含在Rust 1.0中。作为一种解决方法,我编写了enum_primitive crate,它导出一个名为enum_from_primitive!的宏,该宏包装了一个enum声明,并自动添加了一个num::FromPrimitive的实现(来自于num crate)。例如:

#[macro_use]
extern crate enum_primitive;
extern crate num;

use num::FromPrimitive;

enum_from_primitive! {
    #[derive(Debug, PartialEq)]
    enum FooBar {
        Foo = 17,
        Bar = 42,
        Baz,
    }
}

fn main() {
    assert_eq!(FooBar::from_i32(17), Some(FooBar::Foo));
    assert_eq!(FooBar::from_i32(42), Some(FooBar::Bar));
    assert_eq!(FooBar::from_i32(43), Some(FooBar::Baz));
    assert_eq!(FooBar::from_i32(91), None);
}

9
如果你要匹配的整数基于枚举变体的顺序,可以使用 strum 生成一个枚举变体的迭代器,并选择正确的变体:
#[macro_use]
extern crate strum_macros; // 0.9.0
extern crate strum;        // 0.9.0

use strum::IntoEnumIterator;

#[derive(Debug, PartialEq, EnumIter)]
enum MyEnum {
    A = 1,
    B,
    C,
}

fn main() {
    let e = MyEnum::iter().nth(2);
    assert_eq!(e, Some(MyEnum::C));
}

3

我目前正在使用以下代码将整数转换为枚举类型:

#[rustfmt::skip]
#[derive(Debug, Clone, Copy)]
pub enum Square {
    A8, B8, C8, D8, E8, F8, G8, H8,
    A7, B7, C7, D7, E7, F7, G7, H7,
    A6, B6, C6, D6, E6, F6, G6, H6,
    A5, B5, C5, D5, E5, F5, G5, H5,
    A4, B4, C4, D4, E4, F4, G4, H4,
    A3, B3, C3, D3, E3, F3, G3, H3,
    A2, B2, C2, D2, E2, F2, G2, H2,
    A1, B1, C1, D1, E1, F1, G1, H1,
}

impl TryFrom<u64> for Square {
    type Error = String;

    fn try_from(value: u64) -> Result<Self, Self::Error> {
        use Square::*;

        #[rustfmt::skip]
        const LOOKUP: [Square; 64] = [
            A8, B8, C8, D8, E8, F8, G8, H8,
            A7, B7, C7, D7, E7, F7, G7, H7,
            A6, B6, C6, D6, E6, F6, G6, H6,
            A5, B5, C5, D5, E5, F5, G5, H5,
            A4, B4, C4, D4, E4, F4, G4, H4,
            A3, B3, C3, D3, E3, F3, G3, H3,
            A2, B2, C2, D2, E2, F2, G2, H2,
            A1, B1, C1, D1, E1, F1, G1, H1,
        ];

        LOOKUP
            .get(value as usize)
            .ok_or_else(|| {
                format!(
                    "index '{}' is not valid, make sure it's in the range '0..64'",
                    value
                )
            })
            .map(|s| *s)
    }
}

我不知道这有多快,但我可能会在未来找到答案。

当然,这种方法的一个缺点是如果枚举值发生更改,则必须在多个地方进行更新。

再读几遍问题后,我发现这并不是问题所要求的。但是我暂时将此答案保留在此处,因为在搜索“rust将整数转换为枚举”时会看到此线程。尽管如此,该答案仍可用于解决问题。只需将x转换为枚举(x.try_into().unwrap()(实际上你不应该只是简单地解包,而是要处理错误)),然后使用带有枚举变量的正常匹配语句即可。


2

我写了一个简单的宏,将数字值转换回枚举:

macro_rules! num_to_enum {
    ($num:expr => $enm:ident<$tpe:ty>{ $($fld:ident),+ }; $err:expr) => ({
        match $num {
            $(_ if $num == $enm::$fld as $tpe => { $enm::$fld })+
            _ => $err
        }
    });
}

您可以像这样使用它:
#[repr(u8)] #[derive(Debug, PartialEq)]
enum MyEnum {
    Value1 = 1,
    Value2 = 2
}

fn main() {
    let num = 1u8;
    let enm: MyEnum = num_to_enum!(
        num => MyEnum<u8>{ Value1, Value2 };
        panic!("Cannot convert number to `MyEnum`")
    );
    println!("`enm`: {:?}", enm);
}

1
这个宏强制你在调用时重复所有相关的枚举变量。最好将枚举定义提供给宏,这样它可以构建一个知道所有可能变量的东西。 - hkBst

2
你可以使用strum来生成from_repr实现,使用FromRepr派生宏:
use strum::FromRepr;

#[derive(FromRepr, Debug, PartialEq)]
#[repr(u8)]
enum MyEnum {
    A = 1,
    B,
    C,
}

fn main() {
    let e = MyEnum::from_repr(2);
    assert_eq!(e, Some(MyEnum::B));
}

Cargo.toml

[dependencies]
strum = { version = "0.25", features = ["derive"] }

2

理想情况下,您应该能够将枚举值转换为整数:

match x {
    MyEnum::A as i32 => {}
    MyEnum::B as i32 => {}
    MyEnum::C as i32 => {}
    _ => {}
}

然而,这不能编译。您不能在模式中使用表达式(如as)。
但是,您可以在模式中使用consts,并且常量可以使用表达式:
const A: i32 = MyEnum::A as i32;
const B: i32 = MyEnum::B as i32;
const C: i32 = MyEnum::C as i32;
match x {
    A => {}
    B => {}
    C => {}
    _ => {}
}

这个不稳定的inline const可以使得这个变得更好:

#![feature(inline_const_pat)]

match x {
    const { MyEnum::A as i32 } => {}
    const { MyEnum::B as i32 } => {}
    const { MyEnum::C as i32 } => {}
    _ => {}
}

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