是我的一点学习笔记,因为本身内容就不多、不复杂,所以这一篇内容结构与原文基本一致,但是是我个人理解原文的一个思路过程的记录。
枚举,enums
,如果你了解ts
、python
、c/cpp
、java
那你可能会觉得很熟悉,但是又很不同,rust
的枚举更丰富、更灵活、更方便、更强大。
所以你准备走进rust
的枚举类型了吗?
原文链接:Rust程序设计语言
rust
有两种枚举,一个是enum
,一个是option
,我们一个一个来看。
以IPv4和IPv6为例
// 以下是伪代码,不可直接运行
// 1 定义enum
enum IpAddrKind {
V4,
V6,
}
// 2 实例化enum
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
// 3 函数可以以enum为入参
fn route(ip_type: IpAddrKind) { }
// 4 使用函数
route(IpAddrKind::V4);
route(IpAddrKind::V6);
以上是最简单的写法,那么在此之上,我们是否可以将enum与struct结合起来,从而实现更加复杂的enum类型呢?显然是可以的
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind,
address: String,
}
let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
而事实上,我们没有必要把枚举作为结构体的一部分,而是可以直接给枚举成员赋值,就像下面这样
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
而更加有趣的是,enum可以存储不同的类型!比如像这样
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
实际上,标准库也提供了一个开箱即用的定义,让我们来看看学习一下——
struct Ipv4Addr {
// --snip--
}
struct Ipv6Addr {
// --snip--
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
既然enum能存储不同的类型,那自然也能存储不同的struct,这种方式大大地拓展了enum的灵活性和可用性
还有更有趣的!在上一节中,我们说了struct可以使用impl
,同样的,enum也能使用imple
,这意味着你能实现这样的效果——
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
// 在这里定义方法体
}
}
let m = Message::Write(String::from("hello"));
m.call();
option
是rust
提供的另一种枚举值,为什么要option
?是为了解决空值(null
)问题。
你可能在其他语言的开发中,经常遇到空值引起的各种问题。而rust
中,没有空值,所以不会有空值问题。
但是rust
提供了一个可以编码存在或不存在的概念的枚举,这是如何实现的呢?让我们看看标准库——
enum Option<T> {
Some(T),
None,
}
1、这里的
是泛型语法,你可能在别的语言中已经接触过了,这里先不展开。
2、Some
就是,存在一个值;None
,就是并没有一个有效的值,与空值起到同样的作用。
3、Option
和
是不同的类型!所以他们不允许像对一个有效的
那样处理Option
,也就避免了问题。
是不是听起来有点拗口?让我们看看这个例子
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;// 编译器报错
是的,通过此种方式,避免了我们把一个无效的值当成一个有效的值去处理,所以我们可以安全地处理值,并且信赖他绝对不会是空值!而且,在你处理这个值时,需要显式地生命当他为空值的时候的处理方式。
于是乎,通过这种有效值、无效值的枚举方式,实现了使用、判断等场景下的安全性。
这里提到了“判断”,接下来我们就要讲match
表达式了。
match
是一个控制流运算符。比如说——
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!"); // 逻辑处理
1 // 有返回值
}
Coin::Nickel => 5, // 如果代码较短,可以不用括号
Coin::Dime => 10,
Coin::Quarter => 25, // 必须穷尽所有的可能的处理
}
}
是不是有点像c/cpp的switch
语法?让我们问问chatgpt吧,他说:
1、强大的模式匹配:match
支持强大的模式匹配包括结构体、枚举、引用等等;
2、更好的安全性:match
必须处理所有可能的情况;
3、更好的表达能力:match
可以返回值
好的,那让我们继续看看这些神奇的点吧
模式匹配,还有一个功能就是可以绑定匹配的模式的部分值,也就是可以从枚举成员中,提取出值来使用
#[derive(Debug)] // 这样可以立刻看到州的名称
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => { // 增加state变量
println!("State quarter from {:?}!", state);
25
} // 像这样,就可以获取coin的quarter成员中的,内部的值
}
}
上面说的,都是enum
,不要忘了我们的option
哦,通过match
模式的必须穷举处理的特性,避免了遗漏空值处理场景的编码问题
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None, // 如果没有这一条,没有穷尽地匹配,编译器会报错
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
另外,还有统配模式与_
占位符可以使用,要注意的是,other
意味着你需要使用这个变量值,而如果不需要变量值你可以使用_
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other), // 如果要使用变量值
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(), // 如果不需要使用变量值
// _ => (), // 如果不需要处理
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
if let
是更简单的控制流,没有什么好说的,直接看代码就行,比如以下两种方式,效果是相同的——
let mut count = 0;
match coin {
Coin::Quarter(state) => println!("State quarter from {:?}!", state),
_ => count += 1,
}
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}