定义枚举和枚举值

普通的枚举值

铁锈语言的枚举类型看起来和其它编程语言的没什么不同:

enum IpAddrKind {
    V4,
    V6,
}

这段代码定义了一个名为 IpAddrKind 的枚举类型,并且定义了这个类型仅有的两个枚举值:V4V6。要注意这里 V4V6 不是变量,而是值。而且这两个值既不是整型,也不是字符串或是什么别的类型,它的类型就是程序员自定义的 IpAddrKind。通过定义这个枚举类型和值,程序员拓展了铁锈语言的类型系统。

这两个枚举值没有什么其它特殊的地方,称之为 普通枚举值。 既然是值,可以用变量承载字面量:

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

也可以作为函数参数:

fn route(ip_kind: IpAddrKind) {}

route(IpAddrKind::V4); // 传入字面量
route(six);            // 传入变量

同理,该枚举类型也可以作为结构体的字段类型等,不再赘述。

有内涵的枚举值

在定义枚举值的时候,可以在枚举值定义后面增加额外信息,绑定某个 已知 的类型:

enum IpAddr {
    V4(String),
    V6(String),
}

上述代码 V4V6 两个枚举值都绑定了字符串作为各自的内嵌类型。这就好像在金本位规则下,各国给印刷的货币绑定含金量一样,程序员凭空创造的枚举值用其它类型的值作为标的物。该类型枚举值必须通过 枚举类型::枚举值(内嵌类型值) 形式构造:

let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));

⚠️   调皮的程序员一定想问能不能绑定 IpAddr 自己?简单来说是不可以的,因为编译器要求该类型是编译时已知大小的类型。

enum MyEnum {
   A,
   B(MyEnum),
}

let b = MyEnum::B(MyEnum::A);
error[E0072]: recursive type `MyEnum` has infinite size
--> src/main.rs:2:1
  |
2 | enum MyEnum {
  | ^^^^^^^^^^^ recursive type has infinite size
3 |     A,
4 |     B(MyEnum),
  |       ------ recursive without indirection

类似地,用铁锈语言写个链表也真的很麻烦。这个问题我们可以在未来专门讨论。

同一个枚举类型下,不同的枚举值可以绑定不同的内嵌数据类型:

enum IpAddr {
    Unknown,            // 不绑任何类型
    V4(u8, u8, u8, u8), // 绑定一个由4个 `u8` 组成的元组
    V6(String),         // 绑定字符串
    V999(SomeStruct),   // 绑定某个结构体
}

let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));

枚举值的比较

这里说的“比较”,指的是通过 == 号进行的相等判定。由于枚举类型是程序员自定义的类型,其行为也应当由程序员自定义。因此铁锈语言对枚举类型并没有实现统一的 std::cmp::PartialEq 特性。 就是说默认情况下,某枚举类型的两个变量无法直接通过 == 判断是否相等。 下文描述的比较行为,是通过加注解,让编译器在编译期加上了实现 std::cmp::PartialEq 的默认代码(懒人福音)。

对于 没有绑定值 的普通枚举类型,如果两个变量它们值的字面量相同,则它们相等:

#[derive(Debug, PartialEq)]
enum MyEnum {
    A,
    B,
}

let a0 = MyEnum::A;
let a1 = MyEnum::A;
let b0 = MyEnum::B;
assert_eq!(a0, a1); // true
assert_eq!(a0, b0); // false

如果比较双方含有绑定的数据类型,则还需要调用该内嵌类型的 std::cmp::PartialEq 实现方法对两者的内嵌数据进行判定:

#[derive(Debug, PartialEq)]
enum MyEnum {
    A(String),
    B(String),
}

let a0 = MyEnum::A(String::from("Good"));
let a1 = MyEnum::A(String::from("Good"));
let a2 = MyEnum::A(String::from("Bad"));
let b0 = MyEnum::B(String::from("Good"));
assert_eq!(a0, a1); // true
assert_eq!(a0, a2); // false
assert_eq!(a0, b0); // false

字符串类型已经实现了 std::cmp::PartialEq 特性。如果绑定的是自定义的结构体,程序员需要保证该结构体也实现了 std::cmp::PartialEq 特性(比如使用懒人福音)。

当然这种比较行为是编译器实现的默认比较逻辑,程序员可以自定义实现不同的比较逻辑。

高级应用

定义枚举类型的行为

同样作为程序员自定义的类型,枚举和结构体一样,也可以用 impl 关键字添加行为:

#[derive(Debug)]
enum MyEnum {
    A,
    B,
}

impl MyEnum {
    fn call(&self) {
        println!("My type is MyEnum!");
    }
}

这样所有 MyEnum 的枚举值都可以调用该行为逻辑:

let a = MyEnum::A;
let b = MyEnum::B;
a.call();
b.call();

上述代码实现的 call() 方法对于不同的枚举值,其行为是一模一样的。大多数场景下,我们希望枚举的方法可以区分不同的枚举值。此时可以使用 match 关键字:

impl MyEnum {
    fn call(&self) {
        match self {
            Self::A => do_somethig(),
            Self::B => {
                do_something_else();
            },
        }
    }
}

Self (首字母大写)用来在 impltrait 的代码块中表示正在添加实现或逻辑的类型,在这里指代的就是 MyEnum 了。小写开头的 self 用来指示当前实例或值,这里就是 MyEnum::A 或者 MyEnum::B 为值的变量或者字面量。

对于有着复杂内涵的枚举值,如:

enum MyEnum {
    A,                    // 不绑定任何数据
    B { x: i32, y: i32 }, // 绑定某个匿名结构体
    C(String),            // 绑定字符串
    D(i32, i32, i32),     // 绑定某种元组
    E(MyStruct),          // 绑定某实名结构体
}

各个枚举值对应的匹配方式:

impl MyEnum {
    fn call(&self) {
        match self {
            Self::A => {
                // 匹配枚举值 A
                // 随便做点啥,没有额外数据
            },
            Self::B {x, y} => {
                // 匹配绑定了匿名结构体的枚举值 B
                // 使用 x, y (都是i32)做点啥
            },
            Self::C(s) => {
                // 匹配绑定了字符串的枚举值 C
                // 使用字符串 s 做点啥
            },
            Self::D(x, y, z) => {
                // 匹配绑定了三元元组的枚举值 D
                // 使用 x, y, z (都是 i32)做点啥
            },
            Self::E(x) => {
                // 匹配绑定了某结构体的枚举值 E
                // 用 MyStruct 类型的结构体实例 x 做点啥
            },
        }
    }
}

组织代码

绑定数据、添加行为 — 铁锈语言的枚举类型增加这样的功能,它能玩出多种新花样。比如作为一种 组织代码的形式,这种形式中,多种类型都包含相同的行为,但具体行为模式有所区别。

举个例子:猫和狗都是会发声的动物,但是叫声不同。如果用面向对象语言对这个场景进行建模,可以是:

  • 定义接口 SpeakingPet,包含 speak() 方法;
  • 定义 Cat 和 Dog 类型分别实现 SpeakingPet 接口,实现各自的 speak() 方法:猫是打印“我的名字是 XXX,喵喵喵”,狗是打印“我的名字是 YYY,汪汪汪”;
  • 如果有 1000 种不同的发声宠物,还需要定义上千种 class,添加各自的实现方法。

伪代码:

interface SpeakingPet {
    void speak();
}

class Cat implement SpeakingPet {
    private String name;
    public void speak() {
        System.out.println("我的名字是 {}, meow meow meow", this.name);
    }
}

class Dog implement SpeakingPet {
    private String name;
    public void speak() {
        System.out.println("我的名字是 {}, woof woof woof", this.name);
    }
}

用铁锈枚举可以这么组织代码,将这些类型仅仅作为某个枚举的枚举值,并且为枚举统一实现行为:

enum SpeakingPet {
    Cat(String),
    Dog(String),
}

impl SpeakingPet {
    fn speak(&self) {
        match self {
            Self::Cat(s) => println!("我的名字是 {}, meow meow meow", s),
            Self::Dog(s) => println!("我的名字是 {}, woof woof woof", s),
            _ => {
                // 默认匹配,用来快速实现共享行为
            },
        }
    }
}

let tom = SpeakingPet::Cat("Tom");
tom.speak(); // => "我的名字是 Tom, meow meow meow"
let kitty = SpeakingPet::Cat("kitty");
kitty.speak(); // => "我的名字是 kitty,meow meow meow"

再增加不同的宠物类型,只要增加枚举值和 match 代码块种的一段代码:

enum SpeakingPet {
    // 省略其它
    VerySmartCat { name: String, iq: u8 }, // 一种神奇小猫
}

impl SpeakingPet {
    fn speak(&self) {
        match self {
            // 省略其它
            Self::VerySmartCat {name, iq} => {
                println!("我叫 {},我的智商 {}", name, iq);
            },
        }
    }
}

let super_meow_meow = Speaking::VerySmartCat { name: "神奇小猫", iq: 200 };
super_meow_meow.speak(); // => "我叫神奇小猫,我的智商 200"