整数转换为枚举
在 Rust
中,从枚举到整数的转换很容易,但是反过来,就没那么容易,甚至部分实现还挺邪恶,
例如使用transmute
。
1. 一个真实场景的需求
在实际场景中,从整数到枚举的转换有时还是非常需要的,例如你有一个枚举类型,然后需要从外面传入一个整数,用于控制后续的流程走向,此时就需要用整数去匹配相应的枚举(你也可以用整数匹配整数-,
-,看看会不会被喷)。
既然有了需求,剩下的就是看看该如何实现,这篇文章的水远比你想象的要深,且看八仙过海各显神通。
2. C 语言的实现
对于 C
语言来说,万物皆邪恶,因此我们不讨论安全,只看实现,不得不说很简洁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #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 中,以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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
。
3. 使用三方库
首先可以想到的肯定是三方库,毕竟 Rust
的生态目前已经发展的很不错,类似的需求总是有的,这里我们先使用num-traits
和num-derive
来试试。
在Cargo.toml
中引入:
1 2 3 [dependencies] num-traits = "0.2.14" num-derive = "0.3.3"
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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
。
4. TryFrom + 宏
在 Rust 1.34 后,可以实现TryFrom
特征来做转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 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 (()), } } } 这段代码为自定义枚举类型 `MyEnum` 实现了 `TryFrom<i32 >` trait ,允许将一个 `i32 ` 类型的值尝试转换为 `MyEnum` 类型的值。如果转换成功,返回 `Ok (MyEnum)`;如果失败,返回 `Err (())`。下面是对代码的详细解释: --- ### 1 . **`TryFrom` trait 的作用** `TryFrom` 是 Rust 标准库中的一个 trait ,用于定义一种可能失败的转换。它的定义如下: ```rustpub trait TryFrom <T> { type Error ; fn try_from (value: T) -> Result <Self , Self ::Error>; } ``` - `T`:源类型(在这里是 `i32 `)。 - `Error`:转换失败时返回的错误类型(在这里是 `()`,表示没有额外的错误信息)。 - `try_from`:尝试将 `T` 类型的值转换为 `Self ` 类型的方法。 --- ### 2 . **代码解析** ```rustuse 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 (()), } } } ``` #### 关键点:1 . **`type Error = ();`**: - 定义了 `TryFrom` 的关联类型 `Error`,这里使用 `()` 表示没有额外的错误信息。 - 如果转换失败,返回 `Err (())`。2 . **`try_from` 方法**: - 接受一个 `i32 ` 类型的值 `v`,尝试将其转换为 `MyEnum`。 - 使用 `match ` 表达式匹配 `v` 的值: - 如果 `v` 等于 `MyEnum::A` 的整数值,则返回 `Ok (MyEnum::A)`。 - 如果 `v` 等于 `MyEnum::B` 的整数值,则返回 `Ok (MyEnum::B)`。 - 如果 `v` 等于 `MyEnum::C` 的整数值,则返回 `Ok (MyEnum::C)`。 - 如果 `v` 不匹配任何枚举值,则返回 `Err (())`。3 . **`as i32 `**: - 将枚举值转换为 `i32 ` 类型,以便与输入的 `v` 进行比较。 - 假设 `MyEnum` 是一个带有显式整数值的枚举,例如: ```rust enum MyEnum { A = 1 , B = 2 , C = 3 , }
3. MyEnum
的可能定义
假设 MyEnum
的定义如下:
rust #[derive(Debug, PartialEq)] enum MyEnum { A = 1, B = 2, C = 3, }
MyEnum::A
的整数值是 1
。
MyEnum::B
的整数值是 2
。
MyEnum::C
的整数值是 3
。
4. 使用示例
```rust fn main() { let a = MyEnum::try_from(1); // Ok(MyEnum::A) let
b = MyEnum::try_from(2); // Ok(MyEnum::B) let c = MyEnum::try_from(3);
// Ok(MyEnum::C) let invalid = MyEnum::try_from(4); // Err(())
println!("{:?}", a); // Ok(A)
println!("{:?}", b); // Ok(B)
println!("{:?}", c); // Ok(C)
println!("{:?}", invalid); // Err(())
} ```
如果输入的整数值是 1
、2
或
3
,则转换成功,返回对应的枚举值。
如果输入的整数值是其他值(如 4
),则转换失败,返回
Err(())
。
5. 总结
这段代码的作用是为枚举类型 MyEnum
实现
TryFrom<i32>
trait,使得可以将 i32
类型的值尝试转换为 MyEnum
类型的值。通过 match
表达式,代码检查输入的整数值是否与枚举的整数值匹配,如果匹配则返回对应的枚举值,否则返回错误。
这种实现方式非常适合需要将整数与枚举值进行映射的场景,例如从外部数据(如配置文件、网络协议等)中读取整数值并转换为枚举类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 以上代码定义了从`i32 `到`MyEnum`的转换,接着就可以使用`TryInto`来实现转换: ```rustuse 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
特征:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #[macro_export] 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, } }
5. 邪恶之王 std::mem::transmute
这个方法原则上并不推荐,但是有其存在的意义,如果要使用,你需要清晰的知道自己为什么使用 。
在之前的类型转换章节,我们提到过非常邪恶的transmute
转换 ,其实,当你知道数值一定不会超过枚举的范围时(例如枚举成员对应
1,2,3,传入的整数也在这个范围内),就可以使用这个方法完成变形。
最好使用#[repr(..)]来控制底层类型的大小,免得本来需要 i32,结果传入
i64,最终内存无法对齐,产生奇怪的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #[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 z { MyEnum::A => { println! ("Found A" ); } MyEnum::B => { println! ("Found B" ); } MyEnum::C => { println! ("Found C" ); } } }
既然是邪恶之王,当然得有真本事,无需标准库、也无需 unstable 的 Rust
版本,我们就完成了转换!awesome!??
6. 总结
本文列举了常用(其实差不多也是全部了,还有一个 unstable
特性没提到)的从整数转换为枚举的方式,推荐度按照出现的先后顺序递减。
但是推荐度最低,不代表它就没有出场的机会,只要使用边界清晰,一样可以大放光彩,例如最后的transmute
函数.