理解并在 Rust 中实现枚举

更新于 2026-01-15

elijah samson 2024-04-22

在编程中,枚举类型是一种由一组值组成的类型。这基本上使它们成为联合体。因此,许多编程语言为开发者提供了类似枚举的东西以供使用。在这些语言中,枚举通常只是整数或字符串上的类型别名。

Rust 通过提供可以真正包含其他类型的变参类型,并让它们尽可能地不同,将枚举提升到了一个全新的水平。它们在 Rust 中如此重要,以至于它们成为了许多程序的基本构建块。特别是因为 Rust 缺少继承机制,当开发者希望编写一次逻辑并将其用于许多相关任务而不使用像组合这样的技术时,枚举往往是首选。

每个 Rust 开发者都应该对枚举有深入的了解,这篇文章就是关于这个主题的。

枚举基础

编程中的枚举,也称为枚举类型,是由一组命名值(也称为成员或成员类型)组成的数据类型。枚举基本上是一个可以采用枚举所定义任何形状的联合体。在代码中,任何成员通常与具有实际枚举类型的变量关联,这使得它们可以进行比较。

如果考虑颜色,你可以快速识别出色彩理论中的三种基本颜色:

  • 红色
  • 绿色
  • 蓝色

这是枚举示例的一个完美例子。如果你能给任何颜色(通过混合三种基本颜色获得)命名,你也可以将其建模为枚举的值。在代码中,你可以这样表示:

enum Color {
  Red,
  Green,
  Blue,
  White,
  Black,
}

上面的代码并不试图展示如何使用枚举,而是展示了基本声明以及如何表达一个枚举类型。这种声明足以让读者对其有所了解,并且它比使用字符串(例如当前示例中的十六进制代码)或更糟糕的是仅使用颜色名称要好得多。后两种示例肯定会产生一些疑问,因为它不立即显而易见程序员实际上打算做什么。

许多编程语言实现的枚举仅仅是一种更类型安全和可理解的方式来定义常量。在这些语言的背后,枚举成员通常与整数或字符串关联。以 TypeScript 为例,下面的代码显示了使用枚举声明常量值的两个等效示例。

enum Color {
  Red = 1,
  Green = 2,
  Blue = 3,
}

const Color = {
  Red: 1,
  Green: 2,
  Blue: 3,
};

如果现在使用这个枚举,你可以主要用它来比较两个不同的值。

if (colorOne === colorTwo) {
  doSomething();
} else {
  doSomethingElse();
}

这通常是许多编程语言中枚举的主要用途。枚举嵌套在对象内,然后可以用来区分这些对象的不同实例。

有些语言还将枚举进一步发展。例如,Java 实现的枚举是特殊形式的类,而 Rust 则更进一步,使其成为完全的变参类型。

Rust 中的枚举

基本用法

Rust 中的枚举比许多其他枚举实现更为先进,但它们同样允许相同的基本用例。

你可以定义一个想要随时比较值的枚举,如下所示(只需要派生 PartialEq 即可):

#[derive(PartialEq)]
enum PostType {
  Tutorial,
  News,
  Guide,
  Random,
}

然后,你可以相对简单地获取特定成员的引用:

let post_type = PostType::News;

之后,你可以轻松地将此变量与其他同类型的变量进行比较:

if post_type == other_post_type {
  ...
} else {
  ...
}

对于你编写的软件中的基本逻辑来说,这种用法通常已经足够了,但正如你即将了解到的,枚举还有更多功能。

高级用法

回到最初的颜色模型类比,你现在可能会问自己如何可能模拟不同的颜色模型。例如,RGB 定义颜色是通过混合红色、绿色和蓝色。RGBA 表示颜色类似于 RGB,但另外添加了一个透明度通道。CMYK 是通过结合青色、品红、黄色和黑色来定义颜色的。

你可以尝试创建一个结构体,该结构体结合了不同颜色模型的所有特性,并提供方法来内部处理所有差异,但这将是一个耗时的项目。更好的方法是使用 Rust 的枚举作为变参类型,这允许枚举包含数据,甚至可以让成员类型根据需要尽可能不同。

在 Rust 中模拟不同的颜色模型可以这样工作:

#[derive(PartialEq)]
enum ColorModel {
  RGB(u8, u8, u8),
  RGBA(u8, u8, u8, u8),
  // 只是定义成员的另一种方式,并为成员的实际属性命名
  CMYK { cyan: u8, magenta: u8, yellow: u8, key: u8 },
}

每个枚举成员可以包含未命名或已命名的属性。唯一区别在于实例化和解构的工作方式略有不同。有了这个枚举,你现在可以模拟 RGB、RGBA 和 CMYK 颜色。

你只需实例化一个新的枚举,然后就可以轻松地比较两个值,如下所示:

let rgb_red = ColorModel::RGB(255, 0, 0);
let rgba_red = ColorModel::RGBA(255, 0, 0, 255);
// 对于带有命名成员的情况,你需要使用花括号和属性名称
let cmyk_black: ColorModel = ColorModel::CMYK{cyan: 0, magenta: 0, yellow: 0, key: 255};

rgb_red == rgba_red; // false

或者更进一步,使用 match 语句根据你处理的颜色和模型执行工作:

match color {
  ColorModel::RGB(red, green, blue) => do_something_with_rgb(red, green, blue),
  ColorModel::RGBA(red, green, blue, alpha) => do_something_with_rgba(red, green, blue, alpha),
  ColorModel::CMYK { cyan, magenta, yellow, key } => do_something_with_cmyk(cyan, magenta, yellow, key),
}

但这并不是枚举能够做的全部。你还可以向任何枚举添加实现,以构建影响所有成员类型的公共逻辑。在这种情况下,任何实现都必须处理所有可能的成员,这就是为什么此类代码有时会变得杂乱无章的原因。在这些方法中看到很多 match 语句并不罕见。

假设你想添加一个方法来从任何颜色枚举中获取一个十六进制颜色代码。在这种情况下,你可以这样实现枚举:

#[derive(PartialEq, Clone)]
enum ColorModel {
    RGB(u8, u8, u8),
    RGBA(u8, u8, u8, u8),
    CMYK{ cyan: u8, magenta: u8, yellow: u8, key: u8},
}

impl ColorModel {
    pub fn to_hex(&self) -> String {
        match self {
            ColorModel::RGB(red, green, blue) => format!("#{:X}{:X}{:X}", red, green, blue),
            _ => self.to_rgb().to_hex(),
        }
    }

    fn to_rgb(&self) -> Self {
        match self {
            ColorModel::RGB(_, _, _) => self.clone(),
            ColorModel::RGBA(red, green, blue, alpha) => {
                let red: u8 = (1 - alpha ) * 255 + alpha * red;
                let green: u8 = (1 - alpha ) * 255 + alpha * green;
                let blue: u8 = (1 - alpha ) * 255 + alpha * blue;

                ColorModel::RGB(red, green, blue)
            },
            ColorModel::CMYK { cyan, magenta, yellow, key } => {
                let red = 255 * (1 - cyan) * (1 - key);
                let green = 255 * (1 - magenta) * (1 - key);
                let blue = 255 * (1 - yellow) * (1 - key);

                ColorModel::RGB(red, green, blue)

            },
        }
    }
}

这只是对 Rust 枚举能力的一瞥,但它应该给你一个相对好的概念,知道它们能够做什么以及你可以用它们做什么。有趣的是,枚举在内存级别也有一些影响,接下来你会学到这一点。

内存基础

当然,Rust 的枚举在内存中也有表示,了解其具体处理方式是非常好的,因为它可以对你程序的内存配置有很大影响。

技术上,每个枚举都需要所谓的判别式。这是一个整数,使编译器能够确定你的代码当前处理的是枚举的哪个变体。这也使得不同成员类型的比较更加容易。如果判别式不匹配,则无需浪费更多的 CPU 周期来比较进一步的属性。

枚举中每个嵌套属性都会占用额外的内存,所有属性的交叉产品定义了所有变体占用的内存总量。没有魔法能使没有嵌套类型的枚举成员变得更小。不过,最好还是看一个例子。

在下图中,你可以看到一个有两个成员的简单枚举。Simple 是一个没有嵌套属性的枚举,Complex 是一个带有两个嵌套属性的成员,这些属性包含无符号64位整数。即使 Simple 类型不使用这些空间,但在计算机的内存中,它也需要判别式的空间和这两个属性的空间。

随后,Complex 将利用可用的空间放置它的属性到额外的内存槽中。

在某些较为罕见的情况下,编译器甚至可以优化掉判别式。在这些情况下,编译器可以使用无效的位模式,这些模式本质上是某些类型不可能出现的位模式,比如布尔值除了 0 或 1 以外的任何值。

如果出现这样的机会,编译器就会优化掉判别式。相反,它设置特定属性的位模式,使得很明显内存布局实际上指的是哪种类型。

编译器没有判别式来知道它处理的是 Simple,但它知道 bool 只能取 0 或 1,并且它也知道只有一个其他成员类型。这就足以唯一地识别这种内存布局为成员类型 Simple。

如你所见,有时候会有这样的机会,但它们很少见。然而,在某些情况下,开发人员确实可以通过构造一种实际上消除了对判别式需求的枚举来做出真正的改变。

通常,你需要在其他领域了解枚举的内存布局,比如列表或带有许多 Option 的向量,当大多数 None 值的情况会对程序的内存消耗产生重大影响时。Result 的情况也是如此,其中 Ok 和 Err 情况之间的内存消耗差异很大也会产生负面影响。

总结

Rust 的枚举比其他语言中的枚举更先进。此外,它们还提供了更大的灵活性。这主要是因为它们对于语言和你在使用它时必须应用的编程风格来说非常基础。

在 Rust 中,枚举被实现为可以包含任何形式和形状的数据的变参类型,而且单个成员可以根据需要尽可能地彼此不同。这使它们非常强大并且有趣。