docs: rust note

This commit is contained in:
ZiuChen 2024-04-09 20:36:50 +08:00
parent 0649092513
commit 3c33902409

View File

@ -1648,6 +1648,344 @@ fn main() {
### 枚举的定义
在前文中我们介绍了结构体,它可以将字段和数据聚合在一起:
`Rectangle` 结构体中包含 `width``height` 两个字段,还可以包含方法。
而枚举可以提供一种途径,用于声明某个值是一个集合中的一员:
`Rectangle` 是一些形状的集合,例如:`Circle``Triangle`
下面的代码声明了一个枚举类型 `IpAddrKind`,它包含 `V4``V6` 两个成员:
```rs
enum IpAddrKind {
V4,
V6
}
```
可以 `IpAddrKind` 枚举创建不同成员的实例:
```rs
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
```
枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。
下面的例子中,枚举成员作为参数传递给 `route` 函数,不论类型是 `V4` 还是 `V6`
```rs
fn route(ip_kind: IpAddrKind) {}
route(IpAddrKind::V4);
route(IpAddrKind::V6);
```
到目前为止,我们的枚举都是以**类型**形式存在的,并没有一个实际存储 IP 地址数据的地方,我们可以定义一个 `IpAddr` 结构体来完成这样的工作:
```rs
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"),
}
```
实际上,我们可以通过一种更为简洁的方式来表达相同的概念:
只使用枚举,并将数据直接放进每个枚举成员中,而不是用结构体:
```rs
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
```
这样,我们就不再需要额外的结构体来完成:将数据附加到枚举成员上 这件事了。
另外,你也会发现,定义枚举成员的名字也变成了一个函数调用:`IpAddr::V4()` 获取一个 `String` 类型的参数,并返回 `IpAddr` 类型实例。
用枚举来替代结构体还带来了另一个优势:可以给不同的枚举对象绑定不同的数据类型
```rs
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
```
[标准库](https://doc.rust-lang.org/std/net/enum.IpAddr.html)是这样处理IP地址的
```rs
struct Ipv4Addr {
// --snip--
}
struct Ipv6Addr {
// --snip--
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
```
另一个枚举的例子 `Message`
```rs
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
```
我们当然可以用 `struct` 复写这样的枚举:
```rs
struct QuitMessage; // 类单元结构体
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体
```
然而,这些结构体都有不同的类型,而枚举的不同成员是同一个类型。
枚举和结构体另一个相似的地方是:
- 结构体可以使用 `impl` 来定义方法
- 枚举可以通过名为 `call` 的方法来定义
```rs
impl Message {
fn call(&self) {
// some code ...
}
}
let m = Message::Write(String::from("Hello"));
m.call();
```
下面介绍一个标准库中常见且非常实用的枚举 `Option`
`Option` 类型应用广泛,是因为它编码了一个普遍的场景:一个值要么有值要么没值。
例如:请求一个非空列表的第一项,会得到一个值;请求一个空的列表,那么就什么都不会得到。
从类型系统的角度来表达这个概念,意味着编译器需要检查是否处理了所有应该处理的所有情况。
Rust 并没有其他语言中空值(`null`)的功能,在有空值的语言中,变量总是这两种状态之一:空值和非空值。
在 Rust 中存在一个可以编码存在或不存在概念的枚举:`Option<T>`,被定义在标准库中,并且包含在了 prelude 中,这意味着你不需要显式将其引入作用域。
```rs
enum Option<T> {
None,
Some(T),
}
```
甚至,你不需要 `Option::` 的前缀来使用 `Some``None`,下面是一些 `Option` 的例子:
```rs
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
```
`some_number``some_char` 的类型由 Rust 自动推断,而 `absent_number` 则是通过泛型指定告诉 Rust 的。
编译器不允许像一个肯定有效的值那样使用 `Option<T>`,因为本质上 `Option<T>``T` 是不同的类型:
```rs
// 此代码无法通过编译
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y; // ❌
```
编译器确保 `i8` 类型始终有值,不需要做空值检查。只有当使用如 `Option<i8>` 这样的值的时才会担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。
换句话说,在对 `Option<i8>` 进行运算之前,必须将其转换为 `T`,这能帮助我们捕获到空值最常见的问题之一:假设某值不为空但实际上为空的情况。
这样就消除了错误地假设一个非空值的风险:为了拥有一个可能为空的值,你必须要显式地将其放入对应类型的 `Option<T>` 中,当你要使用这个值的时候,必须明确处理值为空的情况。
只要一个值不是 `Option<T>` 类型,你就**可以**安全第认定它的值不为空。
更多地关于 `Option<T>` 的用法,可以看[它的文档](https://doc.rust-lang.org/std/option/enum.Option.html)。
如果你希望一些代码仅在有值 `Some(T)` 时才运行,并允许这些代码使用其中的 `T`,或者是你希望当值为 `None` 时做一些错误处理时,可以试试用 `match`
下文中我们将介绍 `match` 控制流结构,`match` 表达式是一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到值中的数据。
### match 控制流结构
### if let 简洁控制流
Rust 拥有一个名为 `match` 的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较,并根据匹配的模式执行相应的代码。
模式pattern可以由字面值、变量、通配符和许多其他内容组成`match` 的力量来源于模式的表现力与编译器检查,它确保了所有可能得情况都能得到处理。
`match` 表达式想象成某种硬币分类器:硬币滑入有着不同大小孔洞的轨道,每个硬币都会调入符合其大小的孔洞中。同样地,值也会通过 `match` 的每一个模式,并且在遇到第一个符合的模式时,值会进入相关联的代码块,并在执行中被使用。
一个验钞机的例子:
```rs
// 硬币 => 美分
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
```
`match` 中的匹配分支,可以绑定匹配的模式的部分值:
```rs
#[derive(Debug)] // 这样可以立刻看到州的名称
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
```
这里我们给 `Coin::Quarter` 传入州名,以区分不同州对 25 美分的特殊设计。
`match` 表达式中,一旦匹配了此模式,变量 `state` 变量将会绑定对应的州值,随后就可以在当前匹配分支中使用 `state`
```rs
value_in_cents(Coin::Quarter(UsState::Alaska));
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
}
}
}
```
输出为:
```sh
> State quarter from Alaska!
```
下面我们尝试用 `match` 来匹配 `Option<T>` 的值:
编写一个函数,它获取一个 `Option<i32>`,如果其有值,则将值 +1如果没有值则返回 `None`,不执行任何操作。
```rs
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);
```
传入 `Some(5)` 后,`match` 表达式匹配到了 `Some(i)` 这一模式,进入分支,在分支中获得 `T` 的值也就是 `5`,将其加一后返回一个 `Some(6)`
::: warning
Rust 中的匹配是穷尽的exhaustive
在使用 `match` 表达式时,你必须为值的每一种模式都编写一个分支:
```rs
// 此代码无法通过编译
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
```
由于缺少了对 `None` 情况的处理,编译会报错:
```sh
> error[E0004]: non-exhaustive patterns: `None` not covered
```
实际上这也使我们免于假设拥有一个实际上为空的值。
:::
可以使用 `_` 作为占位符,作为兜底模式,匹配所有未被其之前匹配到的值:
```rs
let dice_roll = 9;
match dice_roll {
3 => do_something(),
7 => do_something_else(),
_ => reroll() // 🥳 匹配所有剩余的情况
}
fn do_something() {}
fn do_something_else() {}
fn reroll() {}
```
同时,这个例子满足前文中 Rust 的穷举性要求,因为我们在最后一个分支中明确忽略了其他值,而没有忽略任何值。
可以用单元值替换空操作:
```rs
match dice_roll {
3 => do_something(),
7 => do_something_else(),
_ => () // 单元值(本质是空元组)
}
```
这样在未匹配 3 或 7 时将不会做任何事情。
### if let 简洁控制流