Skip to content

Commit

Permalink
Merge pull request #80 from ziyouwa/patch-2
Browse files Browse the repository at this point in the history
Update item1-use-types.md
  • Loading branch information
lispking authored Oct 10, 2024
2 parents 5104660 + bcf1d4e commit f1bc4f0
Showing 1 changed file with 20 additions and 20 deletions.
40 changes: 20 additions & 20 deletions src/chapter_1/item1-use-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

> “谁叫他们是程序员,而不是打字员” —— [@thingskatedid](https://twitter.com/thingskatedid/status/1400213496785108997)
本章从编译器提供的基本类型开始,到可以组合成数据结构的类型来快速介绍 Rust 的类型系统
本章从编译器提供的基本类型开始,再到各种将数据组合成数据结构的方式,对 Rust 的类型系统做了一个快速导览

在 Rust 中,枚举(`enum`)类型扮演了重要的角色。虽然最初版本的枚举类型和其他语言相比,没有什么区别。但是后续增加的在枚举类型值中携带数据字段的能力极大的提高了其灵活性和表达能力
在 Rust 中,枚举(`enum`)类型扮演了重要的角色。虽然基础版本和其他语言相比没有什么区别,但枚举变量与数据字段相结合的能力,提供了更强的灵活性和表达能力

## 基础类型

对于熟悉其他静态类型编程语言(如 C++、Go 或 Java)的人来说,Rust 类型系统的基本概念应该不太陌生。有一系列具有特定大小的整数类型,包括有符号([`i8`][i8][`i16`][i16][`i32`][i32][`i64`][i64][`i128`][i128])和无符号([`u8`][u8] , [`u16`][u16][`u32`][u32][`u64`][u64][`u128`][u128])。
对于熟悉其他静态类型编程语言(如 C++、Go 或 Java)的人来说,Rust 类型系统的基本概念应该不太陌生。它包括一系列具有特定大小的整数类型,包括有符号([`i8`][i8][`i16`][i16][`i32`][i32][`i64`][i64][`i128`][i128])和无符号([`u8`][u8] , [`u16`][u16][`u32`][u32][`u64`][u64][`u128`][u128]类型

还有两种整数类型,其大小与目标系统上的指针大小匹配:有符号([`isize`][isize])和无符号([`usize`][usize]Rust 并不是那种会在指针和整数之间进行大量转换的语言,所以这种大小等价的特性并不重要。然而,标准集合的大小是 `usize`(来自 `.len()` 方法)类型的,所以在处理集合索引的时候, `usize` 值非常常见 —— 从容量的角度来看,这是显然没有问题的,因为内存中的集合不可能包含比系统上内存寻址范围更多的项
还有两种整数类型:有符号([`isize`][isize])和无符号([`usize`][usize],其大小与目标系统上的指针大小一致。实际上,在 Rust 中,并不会在指针和整数之间进行大量的转换,所以大小是否相等并不重要。但是,标准集合是用 `usize`(来自 `.len()` 方法)类型来返回它们的大小,所以在处理集合索引的时候, `usize` 非常常见 —— 从容量的角度来看,这显然是没问题的,因为内存中集合的项数不可能比系统内存的寻址范围更大

整数类型确实让我们第一次意识到 Rust 是一个比 C++ 更严格的世界 —— 尝试将一个更大的整数类型`i32`赋值给较小的整数类型`i16`)会在编译时产生错误。
整数类型让我们第一次意识到 Rust 是一个比 C++ 更严格的世界 —— 在 Rust 中,尝试将一个更大范围的整数类型`i32`的值赋给较小范围的整数类型`i16`)会在编译时产生错误。


```rust
Expand All @@ -34,14 +34,14 @@ help: you can convert an `i32` to an `i16` and panic if the converted value does
| ++++++++++++++++++++
```

这让人感到安心:当程序员进行有风险的操作时,Rust 不会安静地坐视不管。虽然本例中给出的数值没有超出目标类型 `i16` 能够表达的范围,但是 Rust 编译器仍然会考虑如果转换**成功应该怎么办
这让人感到安心:当程序员进行有风险的操作时,Rust 不会坐视不理,虽然本例中给出的数值没有超出目标类型 `i16` 能够表达的范围,转换不会有问题,但 Rust 编译器仍然会考虑到转换有**成功的可能性

```rust
let x: i32 = 66_000;
let y: i16 = x; // `y` 的值是什么?
```

从错误提示中也可以看出 Rust 拥有严格的规则,但是编译失败的时候也会给出有用的信息,来指引你如何遵守这些规则。编译器的解决方案也引出另外一个问题:如何处理转换过程中可能超出范围的情况?关于错误处理([第 4 条][第 4 条])和使用 `panic!`[第 18 条][第 18 条]我们将在后面有更多的讨论
从错误提示中,我们也可以获得对 Rust 的初步认识: Rust 虽然拥有严格的规则,但编译失败时也会给出有用的提示信息,指引我们如何遵守这些规则。编译器还给出了解决方案,引导我们如何处理转换过程中可能出现的超出范围的情况。关于错误处理([第 4 条][第 4 条])和使用 `panic!`[第 18 条][第 18 条]后续我们将会有更多的讨论

Rust 也不允许一些可能看起来“安全”的操作,哪怕是从更小的整数类型向更大的转换:

Expand All @@ -65,22 +65,22 @@ help: you can convert an `i32` to an `i64`
| +++++++
```

在这里,建议的解决方案并没有给出错误处理的方法,但转换仍然需要是显式的。我们将在后面章节更详细地讨论类型转换([第 5 条][第 5 条])。
在这里,转换并不会有引发错误的担忧,但转换仍然需要是显式的。我们将在后面章节更详细地讨论类型转换([第 5 条][第 5 条])。

现在继续探讨其他原始类型,Rust 有布尔类型([`bool`][bool])、浮点类型([`f32`][f32][`f64`][f64])和[单元类型][unit type] [`()`][unit](类似于 `C``void`)。

更有趣的是 [`char`][char] 字符类型,它持有一个 [`Unicode`][Unicode](类似于 Go 的 [`rune` 类型][rune])。尽管它在内部以 4 字节存储,但与 32 位整数仍然不支持静默转换。

类型系统中的这种精确性迫使你明确地表达你想要表达的内容 —— `u32` 值与 `char` 不同,后者又与序列 UTF-8 字节不同,这又与任意字节序列不同,而且需要你准确地表明你的意图 [^1][Joel Spolsky 的著名博客]文章可以帮助你理解需要哪种类型
类型系统的这种精确性迫使你明确地表达你想要表达的内容 —— `u32` 值与 `char` 不同,后者又与序列 UTF-8 字节不同,这又与任意字节序列不同,最终取决于你的准确意图 [^1][Joel Spolsky 的著名博客]文章可以帮助理解具体需要哪种数据类型

当然,有一些辅助方法允许你在这不同的类型之间进行转换,但它们的签名迫使你处理(或明确忽略)失败的可能性。例如,一个 `Unicode` 代码点 [^2] 总是可以用 32 位表示,所以 `'a' as u32` 是允许的,但反向转换就比较复杂了(因为 `u32` 值不一定是有效的 Unicode 代码点),例如:
当然,有一些辅助方法允许在这不同的类型之间进行转换,但它们的签名迫使你处理(或明确忽略)失败的可能性。例如,一个 `Unicode` 代码点 [^2] 总是可以用 32 位表示,所以 `'a' as u32` 是允许的,但反向转换就比较复杂了(因为 `u32` 值不一定是有效的 Unicode 代码点),例如:

* [char::from_u32][char::from_u32] 返回一个 `Option<char>`,迫使调用者处理失败的情况。
* [char::from_u32_unchecked][char::from_u32_unchecked] 假设有效性,但由于结果是未定义的,因此被标记为 `unsafe`,迫使调用者也使用 `unsafe`[第 16 条][第 16 条])。

## 聚合类型

继续讨论聚合类型,Rust 有:
接下来讨论聚合类型,Rust 有:

- 数组([*Arrays*][Array]),它们持有单个类型的多个实例,实例的数量在编译时已知。例如 `[u32; 4]` 是四个连续的 4 字节整数。
- 元组([*Tuples*][Tuple]),它们持有多个异构类型的实例,元素的数量和类型在编译时已知,例如 `(WidgetOffset, WidgetSize, WidgetColour)`。如果元组中的类型不够独特 —— 例如 `(i32, i32, &'static str, bool)` —— 最好给每个元素命名并使用,或者使用结构体。
Expand Down Expand Up @@ -160,7 +160,7 @@ error[E0308]: mismatched types
| ^^^^^^^^^^^^^ expected enum `enums::Output`, found enum `enums::Sides`
```

使用新类型模式[第 6 条][第 6 条])来包装一个 `bool` 也可以实现类型安全和可维护性;如果语义始终是布尔型的,通常最好使用这种方式,如果将来可能会出现新的选择(例如 `Sides::BothAlternateOrientation`),则应使用枚举类型。
使用新的类型模式[第 6 条][第 6 条])来包装一个 `bool` 也可以实现类型安全和可维护性;如果语义始终是布尔型的,通常最好使用这种方式,如果将来可能会出现新的选择(例如 `Sides::BothAlternateOrientation`),则应使用枚举类型。

Rust 枚举的类型安全性也延续到 `match` 表达式中。下面这段代码无法编译:

Expand Down Expand Up @@ -193,13 +193,13 @@ error[E0004]: non-exhaustive patterns: `Teapot` not covered
= note: the matched value is of type `HttpResultCode`
```

编译器强制程序员处理枚举类型的*所有*可能性 [^3],即使只是添加一个默认分支 `_ => {}` 来处理其他情况。(现代 C++ 编译器也能够并且会对枚举缺失的 `switch` 分支发出警告。)
编译器强制程序员处理枚举类型*所有*的可能性 [^3],即使只是添加一个默认分支 `_ => {}` 来处理其他情况。(现代 C++ 编译器也能够并且会对枚举缺失的 `switch` 分支发出警告。)

## 带有字段的枚举

Rust 枚举特性的真正强大之处在于每个变体都可以携带数据,使其成为一个[*代数数据类型*][代数数据类型](ADT)。这对于主流语言的程序员来说不太熟悉。在 C/C++ 的术语中,它类似于枚举与联合(`union`)的组合,并且在 Rust 是类型安全的。

这意味着程序数据结构的不变式可以被编码到 Rust 的类型系统中;不符合不变式状态的代码甚至无法编译。一个设计良好的枚举使得创建者的意图对于人类以及编译器都是清晰的:
这意味着程序数据结构的不变性可以被编码到 Rust 的类型系统中;不符合不变性状态的代码甚至无法编译。一个设计良好的枚举使得创建者的意图对于人类以及编译器都是清晰的:

```rust
pub enum SchedulerState {
Expand Down Expand Up @@ -247,31 +247,31 @@ struct DisplayProperties {

## 常用的枚举类型

鉴于枚举的强大功能,有两个概念非常常见,以至于 Rust 内置了枚举类型来表达它们,并且这些类型在 Rust 代码中随处可见。
鉴于枚举的强大功能,有两个概念非常常见,以至于 Rust 标准库内置了枚举类型来表达它们,这些类型在 Rust 代码中随处可见。

### `Option<T>`

第一个是 [`Option`][Option] :要么存在特定类型的值(`Some(T)`),要么不存在(`None`)。**始终为可能缺失的值使用 `Option`**,永远不要退回到使用哨兵值(`-1``nullptr`,……)来表达值的缺失。

然而,有一个微妙的点需要考虑。当您在处理一个包含多个事物的**集合**时,您需要考虑一个情况:集合中没有任何事物(即事物的数量为零)是否等同于没有这个集合。在大多数情况下,这两种情况并无不同,您可以继续使用 `Vec<Thing>`:0 长度的集合表示元素的缺失。

然而,确实存在其他罕见的情况,需要用 `Option<Vec<Thing>>` 来区分这两种情况 —— 例如,加密系统可能需要区分[“负载单独传输”][payload]和“提供空负载”。(这与 SQL 中 [`NULL` 标记][null marker]列的争论有关。)
然而,确实存在其他罕见的情况,需要用 `Option<Vec<Thing>>` 来区分这两种情况 —— 例如,加密系统可能需要区分[“负载单独传输”][payload]和“空负载”。(这与 SQL 中 [`NULL` 标记][null marker]列的争论有关。)

一个常见的边缘情况是 `String` 可能缺失 —— 是用 `""` 还是 `None` 来表示值的缺失更有意义?无论哪种方式都可以,但 `Option<String>` 清楚地传达了可能缺失该值的可能性。
同样的,一个常见的边缘情况是 `String` 可能缺失 —— 是用 `""` 还是 `None` 来表示值的缺失更有意义?无论哪种方式都可以,但 `Option<String>` 清楚地传达了可能缺失该值的可能性。

### `Result<T, E>`

第二个常见的概念源于错误处理:如果一个函数执行失败,应该如何报告这个失败?历史上,使用了特殊的哨兵值(例如,Linux 系统调用 的 `-errno` 返回值)或全局变量(POSIX 系统的 `errno`)。最近,一些支持从函数返回多个值或元组值的语言(例如 Go),通常会采用返回一个 `(result, error)` 对的惯例。假设在出现错误时,结果存在某种合适的“零”值。

在 Rust 中,**始终将可能失败的操作的 结果编码为 [`Result<T, E>`][Result]**`T` 保存成功的结果(在 `Ok` 变体中),`E` 在失败时保存错误详情(在 `Err` 变体中)。
在 Rust 中,有一个枚举专门用于此目的:**始终将可能失败的操作的 结果编码为 [`Result<T, E>`][Result]**`T` 保存成功的结果(在 `Ok` 变体中),`E` 在失败时保存错误详情(在 `Err` 变体中)。

使用标准类型使得设计意图清晰,并且允许使用标准转换([第 3 条][第 3 条])和错误处理([第 4 条][第 4 条]);它还使得使用 `?` 运算符来简化错误处理成为可能。
使用标准类型使得设计意图更清晰,并且允许使用标准转换([第 3 条][第 3 条])和错误处理([第 4 条][第 4 条]);它还使得使用 `?` 运算符来简化错误处理成为可能。

---

#### 注释

[^1]: 如果涉及到文件系统,情况会更加复杂,因为流行平台上的文件名介于任意字节和 UTF-8 序列之间:请参阅 [std::ffi::OsString][std::ffi::OsString] 文档。
[^1]: 如果涉及到文件系统,情况会更加复杂,因为在流行各种平台上,文件名介于任意字节和 UTF-8 序列之间:请参阅 [std::ffi::OsString][std::ffi::OsString] 文档。

[^2]: 技术上,是一个 *Unicode 标量值*,而不是代码点。

Expand Down

0 comments on commit f1bc4f0

Please sign in to comment.