枚举
rust的枚举比C/C 的枚举要更加强大。它允许你列举可能的成员来定义一个枚举类型,而这些成员的类型并不要求一致。枚举类型是一个类型,它会包含所有可能的枚举成员, 而枚举值是该类型中的具体某个成员的实例。
定义枚举类型
代码语言:javascript复制enum PokerSuit {
Clubs,
Spades,
Diamonds,
Hearts,
}
定义一个枚举类型需要使用关键字enum,然后是枚举类型的名称,枚举值被包含在花括号中,用逗号分割。注意,定义枚举和结构体一样,最后是不需要分号结束的。
使用枚举类型
使用枚举值的时候,通过::操作符来访问枚举中的具体成员,例如:
代码语言:javascript复制fn main() {
let heart = PokerSuit::Hearts;
let diamond = PokerSuit::Diamonds;
print_suit(heart);
print_suit(diamond);
}
fn print_suit(card: PokerSuit) {
println!("{:?}",card);
}
print_suit 函数的参数类型是 PokerSuit,因此我们可以把 heart 和 diamond 传给它,虽然 heart 是基于 PokerSuit 下的 Hearts 成员实例化的,但是它是货真价实的 PokerSuit 枚举类型。这就是rust的枚举强大之处。不仅如此,同一个枚举类型下的不同成员还能持有不同的数据类型。比如,我们想让扑克牌变得更加实用,那么需要给每张牌赋予一个值,这样就形成了一张真实的扑克牌了。例如:
代码语言:javascript复制enum PokerCard {
Clubs(u8),
Spades(u8),
Diamonds(u8),
Hearts(u8),
}
fn main() {
let c1 = PokerCard::Spades(5);
let c2 = PokerCard::Diamonds(13);
}
这里,每一个枚举成员都是u8类型的,实际上任何类型的数据都可以放入枚举成员中: 例如字符串、数值、结构体甚至另一个枚举。例如:
代码语言:javascript复制enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let m1 = Message::Quit;
let m2 = Message::Move{x:1,y:1};
let m3 = Message::ChangeColor(255,255,0);
}
Quit 没有任何关联数据,Move 包含一个匿名结构体,Write 包含一个 String 字符串,ChangeColor 包含三个 i32。
统一化类型
标准库中有一个例子,如下。
代码语言:javascript复制struct Ipv4Addr {
// --snip--
}
struct Ipv6Addr {
// --snip--
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
这个例子展示了使用枚举将IPV4和IPV6两种不同的IP地址类型进行统一的操作。
Option枚举用于处理空值
在其它编程语言中,往往都有一个 null 关键字,该关键字用于表明一个变量当前的值为空(不是零值,例如整型的零值是 0),也就是不存在值。当你对这些 null 进行操作时,例如调用一个方法,就会直接抛出null 异常,导致程序的崩溃,因此我们在编程时需要格外的小心去处理这些 null 空值。 Tony Hoare,null 的发明者,在他 2009 年的演讲 “Null References: The Billion Dollar Mistake” 中曾经说到:
代码语言:javascript复制I call it my billion-dollar mistake. At that time, I was designing the first comprehensive type system for references in an object-oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
我称之为我十亿美元的错误。当时,我在为一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过我未能抵抗住引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数十亿美元的苦痛和伤害。
Rust 吸取了众多教训,决定抛弃 null,而改为使用 Option 枚举变量来表述这种结果。Option 枚举包含两个成员,一个成员表示含有值:Some(T), 另一个表示没有值:None,定义如下:
代码语言:javascript复制enum Option<T> {
Some(T),
None,
}
其中 T 是泛型参数,Some(T)表示该枚举成员的数据类型是 T,换句话说,Some 可以包含任何类型的数据。Option<T>
枚举是如此有用以至于它被包含在了 prelude(prelude 属于 Rust 标准库,Rust 会将最常用的类型、函数等提前引入其中,省得我们再手动引入)之中,你不需要将其显式引入作用域。另外,它的成员 Some 和 None 也是如此,无需使用 Option:: 前缀就可直接使用 Some 和 None。例如:
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;
如果使用 None 而不是 Some,需要告诉 Rust Option<T>
是什么类型的,因为编译器只通过 None 值无法推断出 Some 成员保存的值的类型。
下面的例子展示了无法直接使用Option<T>
来进行运算。
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x y; // error
这段代码无法执行, Rust 不知道该如何将 Option<i8>
与 i8 相加,因为它们的类型不同。当在 Rust 中拥有一个像 i8 这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需做空值检查。只有当使用 Option<i8>
(或者任何用到的类型)的时候需要担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。换句话说,在对Option<T>
进行运算之前必须将其转换为 T。因此,只要一个值不是 Option<T>
类型,你就 可以 安全的认定它的值不为空。这是 Rust 的一个经过深思熟虑的设计决策,来限制空值的泛滥以增加 Rust 代码的安全性。
那么当有一个 Option<T>
的值时,如何从 Some 成员中取出 T 的值来使用它呢?Option<T>
枚举拥有大量用于各种情况的方法:你可以查看它的文档。熟悉 Option<T>
的方法将对你的 Rust 之旅非常有用。
参考资料
- rust语言圣经
- rust程序设计语言