:::info 本章内容改编自《Programming Rust, 2nd Edition》的第10章。 ::: 上一章介绍的Struct类型,提供了一种将若干个值通过与/and关系组合在一起的机制。本章介绍的Enum类型,则提供了一种将若干个值通过或/or关系组合在一起的机制。

C-style Enum

Enum类型在C语言中已经存在。Rust中的Enum比C中的Enum在表达能力更为强大,毕竟Rust被很很多人称为C语言的现代版本(modern C)。
为了不负modern C的盛名,Rust提供了定义兼容C的Enum类型的能力。这种兼容C的Enum类型,称为C-style Enum。下图给出Rust标准库中定义的一种C-style Enum。
image.png
这是定义在模块std::cmp中的一个Enum类型,其名称为Ordering,用于表示对两个同类型的值进行比较的结果:小于等于大于。其中,属性声明#[repr(i8)]表示采用i8类型对Ordering类型的实例在内存中进行表示。
对应于小于等于、以及大于三种比较结果,Ordering中声明了三个成分:LessEqualGreater。每一个成分称为Ordering类型的一个变体(Variant)或构造器(Constructor)。变体的含义是:Ordering类型的任何一个实例,只可能是这三种情况中的一种。构造器的含义是:可以通过一种成分,构造出Ordering类型的一个实例。在我们介绍了非C-style的Enum之后,你会对构造器这种命名有更深的理解。
下图给出了使用Ordering类型的两个简单示例:
image.pngimage.png
在左边的示例中:

  • 使用use std::cmp::Ordering语句将Ordering类型引入到当前程序中
  • 为了引用到Ordering中定义的一个变体,需要在变体前加上Ordering::

在左边的实例中:

  • 使用use std::cmp::Ordering::*语句将Ordering类型中定义的所有变体引入到当前程序中
  • 这时,在程序中直接通过变体的名称就可以访问到这个变体

    内存表示

    一个C-style Enum类型的实例,在内存中被表示为某种基本整数类型值。

  • 在定义一个C-style Enum时,如果没有声明变体应该被表示为哪一个整数时,则编译器会按照变体在定义时出现的顺序从0开始对变体进行编号

  • 在定义一个C-style Enum时,可以指定每一个变体被表示为哪一个整数。下图给出了一个示例:

image.png
如果没有通过属性声明指定变体应该被表示为何种类型的整数,则编译器会根据变体对应整数的范围,确定一个可以表示所有变体的最小宽度整数类型。当然,可以通过repr属性声明指定C-style Enum的表示类型。具体可参见上面Ordering类型的定义。
我们可以通过std::mem::size_of函数确定某一个变体在内存表示的宽度(单位:字节数)。具体请看下面的程序示例:
image.png

与整数类型之间的转换

使用as操作符,可以将一个C-style Enum类型的值转换为一个整数。请看如下代码示例:

  1. assert_eq!(HttpStatus::OK as i32, 200);

但是,as操作符不支持将一个整数转换为一个C-style Enum类型的值。原因如下:不是每一个整数都有一个对应的Enum值;因此,在很多情况下,这种转换没有意义。
如果需要,可以自定义一个函数来完成这样的转换。请看下面的函数示例:

  1. fn http_status_from_u32(n: u32) -> Option<HttpStatus> {
  2. match n {
  3. 200 => Some(HttpStatus::OK),
  4. 304 => Some(HttpStatus::NotModified),
  5. 404 => Some(HttpStatus::NotFound),
  6. _ => None
  7. }
  8. }

实现trait与附着方法

就像为Struct类型自动实现常用trait一样,我们也可以通过属性声明为Enum实现常用的trait。请看下面的代码示例:

  1. #[derive(Copy, Clone, Debug, PartialEq, Eq)]
  2. enum TimeUnit {
  3. Second, Minute, Hour, Day, Month, Year
  4. }

同样地,我们也可以通过impl代码块为Enum类型附着方法。请看下面的代码示例:

  1. impl TimeUnit {
  2. fn singular(self) -> &'static str {
  3. match self {
  4. TimeUnit::Second => "second",
  5. TimeUnit::Minute => "minute",
  6. TimeUnit::Hour => "hour",
  7. TimeUnit::Day => "day",
  8. TimeUnit::Month => "month",
  9. TimeUnit::Year => "year",
  10. }
  11. }
  12. }

Enum with Data

除了支持C-style Enum,Rust还可以定义包含自定义数据的Enum,称为Enum with Data。
具体而言,Enum中的变体,可以具有三种不同的形式:Unit variant、Tuple variant、以及Struct variant。请看下面的代码示例:

  1. #[derive(Copy, Clone, Debug, PartialEq)]
  2. enum RoughTime {
  3. InThePast(TimeUnit, u32), // 这是一个Tuple variant/constructor
  4. JustNow, // 这是一个Unit variant
  5. InTheFutute(TimeUnit, u32), // 这是一个Tuple variant/constructor
  6. }
  7. let four_score_and_seven_years_ago =
  8. RoughTime::InThePast(TimeUnit::Year, 4 * 20 + 7);
  9. let three_hours_from_now =
  10. RoughTime::InTheFuture(TimeUnit::Hour, 3);
  11. // 现在,你应该可以明白,为什么Enum的变体(variant)也被称为构造器(constructor)了。
  12. // 因为,我们可以使用变体构造出Enum类型的一个实例。
  13. enum Shape {
  14. Sphere {center: Point3d, radius: f32}, // 一个Struct variant/constructor
  15. Cuboid {corner: Point3d, corner: Point3d}, // 一个Struct variant/constructor
  16. }
  17. let unit_sphere = Shape::Sphere {
  18. center: Point3d { x: 0.0, y: 0.0, z: 0.0 },
  19. radius: 1.0
  20. }

Enum in Memory

对于任意一个Enum类型,它都是一个定长类型。也就是说,这个类型的所有指,在内存中都采用相同长度的二进制串进行存储。
在一般意义上,Enum值在内存中的表示包含两个部分:

  • 一个整数,表示当前这个值对应于Enum类型的哪个变体。Rust编译器会根据变体的数量确定一个最小宽度的整数类型对变体进行编号
  • 一片足够长度的内存空间,可以容纳占用内存最多的那个变体

下图展示了RoughTime类型的三个值在内存中的排布情况:
image.png
可以看到,RoughTime类型的每一个值在内存都被表示为一个长度为64bit的二进制串(8个byte)。其中:

  • 第一个byte用于对RoughTime类型的三个变体进行编码
  • 对于一个JustNow类型的值,余下的3个byte不包含任何信息
  • 对于一个InTheFutute类型的值或InThePast类型的值,第二个byte用于存储一个TimeUnit类型的值;最后4个byte用于存储一个u32类型的值。第2、3个byte,基于内存对齐(allignment)的原因,没有被使用

但是,Rust不会对Enum值的内存表示/排布做出任何承诺。Rust会根据具体情况,选择使用一个高效的内存排布方式。

使用Enum定义树形数据结构

下面,我们定义一种用于表示JSON数据的Enum类型。
JSON的全称是JavaScript Object Notation,是一种轻量级的数据交换格式。JSON可以表示6种类型的值:Null、Boolean、Number、String、Array、以及Object。一个Array类型的值包含一组任意合法值的序列。一个Object类型的值包含若干键-值对。其中,是一个String类型的值;是任意一个合法的值。
下面给出了JSON数据的一个示例:

  1. {
  2. "book": [
  3. {
  4. "id": "01",
  5. "lang": "Java",
  6. "edition": "thrid",
  7. "author": "Zhang San"
  8. },
  9. {
  10. "id": "07",
  11. "lang": "C++",
  12. "edition": "second",
  13. "author": "Li Si"
  14. }
  15. ]
  16. }

可以看到,JSON通过对不同数据的顺序排列和嵌套,形成了一种树形的数据结构。
下面,我们给出一种用于表示JSON数据的Enum类型:

  1. enum Json {
  2. Null, // Unit variant
  3. Boolean(bool), //Tuple varaiant
  4. Number(f64),
  5. String(String),
  6. Array(Vec<Json>),
  7. Object(Box<HashMap<String, Json>>)
  8. }

Json类型的6种变体,分别对应于JSON中6种类型的数据。其中,除了第一个变体Null是Unit variant,其它变体都是Tuple varaint。
下图给出了Json类型的几个值在内存中的排布情况(在内存地址宽度为64bit的计算机系统中)。
image.png

  • 在所有的这些值中,第一个字节用来存放变体的编号,余下字节用于存放具体数据。
  • Array变体值中,第2~8个字节由于内存对齐的原因没有使用;第9~32个字节用于存放一个Vec类型的值。
  • Number变体值中,第2~8个字节由于内存对齐的原因没有使用;第9~16个字节用于存放一个f64类型的值。

在上面这个Json类型中,有一处非常奇怪的地方,变体Object中存放的不是HashMap<String,Json>,而是Box<HashMap<String,Json>>。原因大概如下:

  • Box<...>是一种指针类型,它的每一个值在内存中只占用8个字节
  • HashMap<...>是一种较复杂的Struct类型,它的每一个值在内存中占用48个字节
  • 如果把HashMap直接放在变体Object中,则Json类型的每一个值都要占用56个字节
  • 但是,如果把Box放在变体Object中,则Json类型的每一个值只需要占用32个字节。
  • 因此,从节省内存的角度,将Object变体中的类型声明为Box<HashMap<String, Json>>

在这个示例中,如果想近一步节省Json值的内存占用,可以将变体Array声明为Box<Vec<Json>>。请你想一想:这样声明后,每一个Json值会占用几个字节呢?

Generic Enum

在前文中,我们已经看到了Generic Enum类型的很多实例。下图给出了Rust标准库中定义的两个Generic Enum类型。
image.png
其中,左边的Option<T>类型用于表示一个可能不存在的值;右边的Result<T, E>类型用于表示可能存在错误的返回值。它们的具体使用方式,我们就不再解释说明了。
需要指出一点,对于Option<T>类型的值,存在一种编译优化:如果T是一种指针类型的值(如引用、Box、以及其它智能指针),那么,Rust在对Option<T>值进行排布时,不会再存放变体的编号。
例如,对于类型Option<Box<i32>>,它的每一个值在内存中只占用一个指针的长度(即:一个isize值的长度)。其中,当一个值所占用的内存空间中存放的是一个0值,则表明当前这个值为None;否则,表示前这个值是一个指针。为什么可以这样优化呢?因为Rust中不存在空指针(null pointer)。
与C/C++语言相比,这种优化能够保证在不降低运行时性能的同时,消除对空指针的使用。
下图给出了Generic Enum的另一个示例。在这个示例中,我们通过声明一个Generic Enum类型和一个Generic Struct类型,来表示一颗二叉树。
image.png

一个小问题

关于Enum类型,还剩下一个小问题:给定一个enum类型E,以及E的一个实例e,如何判断e属于E中的哪一个变体?

  1. enum Shape {
  2. Sphere { center : (f32, f32, f32), radius: f32 },
  3. Cuboid { corner1: (f32, f32, f32), corner2: (f32, f32, f32) }
  4. }
  5. fn is_sphere(shape: Shape) -> bool {
  6. // 如何才能知道shape是不是属于变体Sphere呢?
  7. }

Rust提供了pattern matching机制来解决这个问题。详细内容,请看下一章。


:::warning 本章内容到此结束 :::