枚举(enumerations),也被称作 enums。
枚举允许你通过列举可能的 成员(variants) 来定义一个类型。
首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做 Option
,它代表一个值要么是某个值要么什么都不是。
简单来讲,枚举就是一种类型,这种类型中可包含多个值,但是在实际去使用枚举的时候只能使用这么多个值中的其中一个。
比如,性别就可以是一种枚举类型,性别的值只能是男或者女(那种不伦不类的就算了,这里不谈)。当你去使用性别这种枚举的时候,要么是男,要么是女,不可能两个都取,也不可能一个都不取。
Rust中的枚举的基本用法:
enum 枚举名 {
枚举值1, //可指定类型也可不指定类型
枚举值2,
...
枚举值n,
}
最后一个枚举值后面的逗号有没有都不影响,但是官网一般都会加上这个逗号,编译器中格式化代码之后也会自动加。
//未指定枚举值的类型
let 变量名 = 枚举名::枚举值名;
//指定枚举值的类型
let 变量名 = 枚举名::枚举值名(枚举值的实际值)
使用枚举的注意事项:在Rust中枚举不能直接通过if表达式来比较,一般使用match
类模式匹配
例子:
enum Sex { //定义了性别的枚举类型
MAN,
WOMAN,
}
struct Person { //定义了人的结构体类型
name: String,
sex: Sex,
}
impl Person {
//实现人的info方法
fn info(&self) {
println!("name: {}", self.name);
match self.sex { //通过模式匹配,不能使用if表达式
Sex::MAN => println!("sex: 男"),
Sex::WOMAN => println!("sex: 女"),
}
}
}
fn main() {
let jack = Person {
name: String::from("Jack"),
sex: Sex::MAN,
};
jack.info();
}
结果:
name: jack
sex: 男
我们可以将数据直接放进每一个枚举成员而不是将枚举作为结构体的一部分。
enum Sex {
MAN(u8),
WOMAN(u8),
}
fn main() {
let man = Sex::MAN(1);
let woman = Sex::WOMAN(0);
match man {
//s会获取man中Sex::MAN的值,即1
Sex::MAN(s) => println!("这是男人{}", s),
_ => println!("这是女人"),
}
match woman {
//使用_则表示不获取值
Sex::WOMAN(_) => println!("这是女人"),
_ => println!("这是男人"),
}
}
以上的例子中可以看出的是两个枚举类型的声明方式不同:
//第一种,没有指定枚举中值的类型
enum Sex {
MAN,
WOMAN,
}
//第二种,指定了枚举中值的类型
//因为结构体也是数据类型的一种
//所以在枚举中当然也可以指定是我们自定义的类型(结构体、元组)
enum Sex {
MAN(u8),
WOMAN(u8),
}
不管采用哪种方式声明枚举,那么在使用到枚举的时候就按对应的方式进行赋值操作。
这一部分会分析一个 Option
的案例,Option
是标准库定义的另一个枚举。Option
类型应用广泛因为它编码了一个非常普遍的场景,即一个值要么有值要么没值。
例如,如果请求一个包含项的列表的第一个值,会得到一个值,如果请求一个空的列表,就什么也不会得到。从类型系统的角度来表达这个概念就意味着编译器需要检查是否处理了所有应该处理的情况,这样就可以避免在其他编程语言中非常常见的 bug.
Rust 并没有很多其他语言中有的空值功能。空值(Null )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。例如C++中的nullptr,Java中的null等。在C语言中的NULL不是没有值而是对0强制转换成void*
.所以C语言也可以说是不安全的。
Tony Hoare,null 的发明者,在他 2009 年的演讲 “Null References: The Billion Dollar Mistake” 中曾经说到:
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中这个问题在语言层面就得以解决。因为Rust没有空值的功能,编译都不给通过,保证了一定的安全。
Rust 虽然没有空值,但是它拥有一个可以编码存在或不存在概念的枚举。这个枚举是 Option
,而且它定义于标准库中,如下:
enum Option<T> {
None,
Some(T),
}
Option
枚举被包含在 prelude (预处理模块)之中,你不需要将其显式引入作用域。另外,它的成员也是如此,可以不需要 Option::
前缀来直接使用 Some
和 None
。即便如此 Option
也仍是常规的枚举,Some(T)
和 None
仍是 Option
的成员。
T
就是我们要实际传进去的类型的占位。其中T
可以是普通的基本类型也可以是由结构体组成的复杂数据类型。在程序编译阶段就会把我们实际传进去的类型覆盖掉T
.泛型就相当于做一个模板,后来者使用的时候就直接按这个模板来做就好。这样做的好处就是节省代码的书写时间。
let some_number = Some(5); //此时T是i32类型
let some_char = Some('e'); //此时T是char类型
let absent_number: Option<i32> = None; //此时T是i32类型,当赋值为None的时候要手动指定变量的类型,
当有一个 Some
值时,我们就知道存在一个值,而这个值保存在 Some
中。当有个 None
值时,在某种意义上,它跟空值具有相同的意义:并没有一个有效的值。
简而言之,因为 Option
和 T
(这里 T
可以是任何类型)是不同的类型,编译器不允许像一个肯定有效的值那样使用 Option
.
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y; //i8类型和Option不是一个类型。不能直接相加,需要通过转换才可以相加。
报错:
error[E0277]: cannot add `Option` to `i8`
|
5 | let sum = x + y;
| ^ no implementation for `i8 + Option`
| # 没有实现`i8+Option`
= help: the trait `Add
当在 Rust 中
Option
类型–>开发者需要考虑空值检查。如果出现使用空值,编译器就直接报错。为了拥有一个可能为空的值,你必须要显式的将其放入对应类型的 Option
中。接着,当使用这个值时,必须明确的处理值为空的情况。只要一个值不是 Option
类型,你就 可以 安全的认定它的值不为空。这是 Rust 的一个经过深思熟虑的设计决策,来限制空值的泛滥以增加 Rust 代码的安全性。
那么当有一个 Option
的值时,如何从 Some
成员中取出 T
的值来使用它呢?Option
枚举拥有大量用于各种情况的方法:你可以查看它的文档。熟悉 Option
的方法将对你的 Rust 之旅非常有用。
从Some(T)
中 获取T
的值:
self
是None
则将该函数的value
赋值给self
,返回值是self
中Some
中的值。self
是否为None都会把该函数的value
赋值给self
,返回值是self
中Some
中的值。例子:
fn main() {
let mut x = None;
let y = x.get_or_insert(88); //x = Some(88), y = 88
println!("y = {}", y);
println!("x = {:?}", x);
let y = x.insert(99); //x = Some(99), y = 99
println!("y = {}", y);
println!("x = {:?}", x);
}
get_or_insert_with的坑,使用官方文档的例子会觉得Rust的语法特别奇怪,一时不知道它在干嘛:
let mut x = None;
{
let y: &mut u32 = x.get_or_insert_with(|| 5);
assert_eq!(y, &5);
*y = 7;
}
assert_eq!(x, Some(7));
其中第4行是亮点。这个||
符号可太秀了。。。秀得我头皮发麻。一看以为是逻辑或运算,但是想想也不对,逻辑或运算是双目的,这里是单目的。然后这条代码是把88
给了y
.
然后就想一下,y
为什么能拿到88
,然后get_or_insert_with
本身是方法(函数),那就可能存在函数嵌套,那就是说||
可能是一种匿名函数,测试了一下,发现它就是匿名函数。
这样写你看不懂
let y = x.get_or_insert_with(|| 88);
这样写就看得懂了
let y = x.get_or_insert_with(|| {88});
或者
let y = x.get_or_insert_with(|| {
88
});
同样的看以下例子理解它:
fn main() {
let mut x = None;
let y = x.get_or_insert_with(|| {
let ret = 45; //语句
println!("我是匿名函数,我要返回{}", ret); //语句
ret //表达式 会返回出去
});
println!("y = {}", y); //45
println!("x = {:?}", x); //Some(45)
}
还有一个官方案例,但是目前这个函数还不能使用:get_or_insert_default
,可以看看官方的说明:Rust官方文档.
这个应该在控制流的时候就应该介绍了,但是也没多大关系,其实就是简化match
的,就是在特定的情境下相对于match
来说少写一些代码。其中特定情境说的就是只模式匹配两种情况,非它即另一些它的情况。下面看例子:
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {}", max),
_ => (),
}
根据上面的代码可以使用if let来简化它:
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {}", max);
}
就是少写了_
的情况。
它的工作方式与 match
相同。使用 if let
意味着编写更少代码。然而,这样会失去 match
强制要求的穷尽性检查。match
和 if let
之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。
换句话说,可以认为 if let
是 match
的一个语法糖,它当值匹配某一模式时执行代码而忽略所有其他值。
意思就是没什么多大用处。反而还多占了一点开发者脑子的内存。开发者没那么多头发,实际开发的时候想到哪个就用哪个就行了。