You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

5.4 KiB

整数转换为枚举

在Rust中从枚举到整数的转换很容易但是反过来就没那么容易甚至部分实现还挺邪恶, 例如使用transmute

一个真实场景的需求

在实际场景中,从枚举到整数的转换有时还是非常需要的,例如你有一个枚举类型,然后需要从外面穿入一个整数,用于控制后续的流程走向,此时就需要用整数去匹配相应的枚举(你也可以用整数匹配整数-, -,看看会不会被喷)。

既然有了需求,剩下的就是看看该如何实现,这篇文章的水远比你想象的要深,且看八仙过海各显神通。

C语言的实现

对于C语言来说万物皆邪恶因此我们不讨论安全只看实现不得不说很简洁

#include <stdio.h>

enum atomic_number {
    HYDROGEN = 1,
    HELIUM = 2,
    // ...
    IRON = 26,
};

int main(void)
{
    enum atomic_number element = 26;

    if (element == IRON) {
        printf("Beware of Rust!\n");
    }

    return 0;
}

但是在Rust中以下代码

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

fn main() {
    // 将枚举转换成整数,顺利通过
    let x = MyEnum::C as i32;

    // 将整数转换为枚举,失败
    match x {
        MyEnum::A => {}
        MyEnum::B => {}
        MyEnum::C => {}
        _ => {}
    }
}

就会报错: MyEnum::A => {} mismatched types, expected i32, found enum MyEnum

使用三方库

首先可以想到的肯定是三方库毕竟Rust的生态目前已经发展的很不错类似的需求总是有的这里我们先使用num-traitsnum-derive来试试。

Cargo.toml中引入:

[dependencies]
num-traits = "0.2.14"
num-derive = "0.3.3"

代码如下:

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),
    }
}

除了上面的库,还可以使用一个较新的库: num_enums

TryFrom + 宏

在Rust1.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(()),
        }
    }
}

以上代码定义了从i32MyEnum的转换,接着就可以使用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,
    }
}

邪恶之王std::mem::transmute

这个方法原则上并不推荐,但是有其存在的意义,如果要使用,你需要清晰的知道自己为什么使用

在之前的类型转换章节,我们提到过非常邪恶的transmute转换,其实,当你知道数值一定不会超过枚举的范围时(例如枚举成员对应123传入的整数也在这个范围内),就可以使用这个方法完成变形。

最好使用#[repr(..)]来控制底层类型的大小免得本来需要i32结果传入i64最终内存无法对齐产生奇怪的结果

#[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"); }
    }
}

既然是邪恶之王当然得有真本事无需标准库、也无需unstable的Rust版本我们就完成了转换awesome!??

总结

本文列举了常用(其实差不多也是全部了还有一个unstable特性没提到)的从整数转换为枚举的方式,推荐度按照出现的先后顺序递减。

但是推荐度最低,不代表它就没有出场的机会,只要使用边界清晰,一样可以大方光彩,例如最后的transmute函数.