铁锈语言之枚举
定义枚举和枚举值
普通的枚举值
铁锈语言的枚举类型看起来和其它编程语言的没什么不同:
enum IpAddrKind {
V4,
V6,
}
这段代码定义了一个名为 IpAddrKind
的枚举类型,并且定义了这个类型仅有的两个枚举值:V4
和 V6
。要注意这里 V4
,V6
不是变量,而是值。而且这两个值既不是整型,也不是字符串或是什么别的类型,它的类型就是程序员自定义的 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),
}
上述代码 V4
和 V6
两个枚举值都绑定了字符串作为各自的内嵌类型。这就好像在金本位规则下,各国给印刷的货币绑定含金量一样,程序员凭空创造的枚举值用其它类型的值作为标的物。该类型枚举值必须通过 枚举类型::枚举值(内嵌类型值)
形式构造:
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
(首字母大写)用来在impl
和trait
的代码块中表示正在添加实现或逻辑的类型,在这里指代的就是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"