4.3.4 枚举和整数

整数转换为枚举

在 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-traitsnum-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,用于定义一种可能失败的转换。它的定义如下:

​```rust
pub trait TryFrom<T> {
type Error;
fn try_from(value: T) -> Result<Self, Self::Error>;
}
​```

- `T`:源类型(在这里是 `i32`)。
- `Error`:转换失败时返回的错误类型(在这里是 `()`,表示没有额外的错误信息)。
- `try_from`:尝试将 `T` 类型的值转换为 `Self` 类型的方法。

---

### 2. **代码解析**
​```rust
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(()),
}
}
}
​```

#### 关键点:

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(())

} ​```

  • 如果输入的整数值是 123,则转换成功,返回对应的枚举值。
  • 如果输入的整数值是其他值(如 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`来实现转换:

```rust
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特征:

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 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!??

6. 总结

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

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


4.3.4 枚举和整数
http://binbo-zappy.github.io/2025/02/05/rust圣经/4-3-4-枚举和整数/
作者
Binbo
发布于
2025年2月5日
许可协议