Update: unified format

pull/352/head
Allan Downey 3 years ago
parent 31fa75724f
commit 8b1b83b773

@ -1,25 +1,25 @@
# 数组
在日常开发中使用最广的数据结构之一就是数组在Rust中最常用的数组有两种第一种是速度很快但是长度固定的`array`,第二种是可动态增长的但是有性能损耗的`Vector`,在本书中,我们称`array`为数组,`Vector`为动态数组。
在日常开发中,使用最广的数据结构之一就是数组,在 Rust 中,最常用的数组有两种,第一种是速度很快但是长度固定的 `array`,第二种是可动态增长的但是有性能损耗的 `Vector`,在本书中,我们称 `array` 为数组,`Vector` 为动态数组。
不知道你们发现没,这两个数组的关系跟`&str`与`String`的关系很像前者是长度固定的字符串切片后者是可动态增长的字符串。其实在Rust中无论是`String`还是`Vector`它们都是Rust的高级类型集合类型在后面章节会有详细介绍。
不知道你们发现没,这两个数组的关系跟 `&str` `String` 的关系很像,前者是长度固定的字符串切片,后者是可动态增长的字符串。其实,在 Rust 中无论是 `String` 还是 `Vector`,它们都是 Rust 的高级类型:集合类型,在后面章节会有详细介绍。
对于本章节,我们的重点还是放在数组`array`上。数组的具体定义很简单:将多个类型相同的元素依次组合在一起,就是一个数组。结合上面的内容,可以得出数组的三要素:
对于本章节,我们的重点还是放在数组 `array` 上。数组的具体定义很简单:将多个类型相同的元素依次组合在一起,就是一个数组。结合上面的内容,可以得出数组的三要素:
- 长度固定
- 元素必须有相同的类型
- 依次线性排列
这里再啰嗦一句,**我们这里说的数组是Rust的基本类型是固定长度的这点与其他编程语言不同其它编程语言的数组往往是可变长度的与Rust中的动态数组`Vector`类似**,希望读者大大牢记此点。
这里再啰嗦一句,**我们这里说的数组是 Rust 的基本类型,是固定长度的,这点与其他编程语言不同,其它编程语言的数组往往是可变长度的,与 Rust 中的动态数组 `Vector` 类似**,希望读者大大牢记此点。
### 创建数组
在Rust中数组是这样定义的
Rust 中,数组是这样定义的:
```rust
fn main() {
let a = [1, 2, 3, 4, 5];
}
```
数组语法跟`javascript`很像,也跟大多数编程语言很像。由于它的元素类型大小固定,且长度也是固定,因此**数组是存储在栈上**,性能也会非常优秀。与此对应,动态数组`Vector`是存储在堆上,因此长度可以动态改变。当你不确定是使用数组还是动态数组时,那就应该使用后者,具体见[动态数组Vector](../collections/vector.md)一章.
数组语法跟 JavaScript 很像,也跟大多数编程语言很像。由于它的元素类型大小固定,且长度也是固定,因此**数组 `array` 是存储在栈上**,性能也会非常优秀。与此对应,**动态数组 `Vector` 是存储在堆上**,因此长度可以动态改变。当你不确定是使用数组还是动态数组时,那就应该使用后者,具体见[动态数组Vector](../collections/vector.md)
举个例子,在需要知道一年中各个月份名称的程序中,你很可能希望使用的是数组而不是动态数组。因为月份是固定的,它总是只包含 12 个元素:
```rust
@ -31,13 +31,13 @@ let months = ["January", "February", "March", "April", "May", "June", "July",
```rust
let a: [i32; 5] = [1, 2, 3, 4, 5];
```
这里,数组类型是通过方括号语法声明,`i32`是元素类型,分号后面的数字`5`是数组长度,数组类型也从侧面说明了**数组的元素类型要统一,长度要固定**.
这里,数组类型是通过方括号语法声明,`i32` 是元素类型,分号后面的数字 `5` 是数组长度,数组类型也从侧面说明了**数组的元素类型要统一,长度要固定**
还可以使用下面的语法初始化一个**某个值重复出现N次的数组**
```rust
let a = [3; 5];
```
`a`数组包含`5`个元素,这些元素的初始化值为`3`,聪明的读者已经发现,这种语法跟数组类型的声明语法其实是保持一致的:`[3;5]` 和`[类型;长度]`.
`a` 数组包含 `5` 个元素,这些元素的初始化值为 `3`,聪明的读者已经发现,这种语法跟数组类型的声明语法其实是保持一致的:`[3;5]` 和 `[类型;长度]`
在元素重复的场景,这种写法要简单的多,否则你就得疯狂敲击键盘:`let a = [3, 3, 3, 3, 3];`,不过老板可能很喜欢你的这种疯狂编程的状态。
@ -52,7 +52,7 @@ fn main() {
let second = a[1]; // 获取第二个元素
}
```
与许多语言类似数组的索引下标是从0开始的。此处`first`获取到的值是`9``second`是`8`。
与许多语言类似数组的索引下标是从0开始的。此处`first` 获取到的值是 `9``second` `8`
#### 越界访问
如果使用超出数组范围的索引访问数组元素,会怎么样?下面是一个接收用户的控制台输入,然后将其作为索引访问数组元素的例子:
@ -84,7 +84,7 @@ fn main() {
}
```
使用`cargo run`来运行代码因为数组只有5个元素如果我们试图输入`5`去访问第`6`个元素,则会访问到不存在的数组元素,最终程序会崩溃退出:
使用 `cargo run` 来运行代码,因为数组只有 5 个元素,如果我们试图输入 `5` 去访问第 6 个元素,则会访问到不存在的数组元素,最终程序会崩溃退出:
```console
Please enter an array index.
5
@ -92,11 +92,11 @@ thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 5'
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
这就是数组访问越界访问了数组中不存在的元素导致Rust运行时错误。程序因此退出并显示错误消息未执行最后的`println!`语句。
这就是数组访问越界,访问了数组中不存在的元素,导致 Rust 运行时错误。程序因此退出并显示错误消息,未执行最后的 `println!` 语句。
当你尝试使用索引访问元素时Rust 将检查你指定的索引是否小于数组长度。如果索引大于或等于数组长度Rust会出现 panic。这种检查只能在运行时进行比如在上面这种情况下编译器无法在编译期知道用户运行代码时将输入什么值。
当你尝试使用索引访问元素时Rust 将检查你指定的索引是否小于数组长度。如果索引大于或等于数组长度Rust会出现 ***panic***。这种检查只能在运行时进行,比如在上面这种情况下,编译器无法在编译期知道用户运行代码时将输入什么值。
这种就是Rust的安全特性之一。在很多系统编程语言中并不会检查数组越界问题你会访问到无效的内存地址获取到一个风马牛不相及的值最终导致在程序逻辑上出现大问题而且这种问题会非常难以检查。
这种就是 Rust 的安全特性之一。在很多系统编程语言中,并不会检查数组越界问题,你会访问到无效的内存地址获取到一个风马牛不相及的值,最终导致在程序逻辑上出现大问题,而且这种问题会非常难以检查。
## 数组切片
@ -109,10 +109,10 @@ let slice: &[i32] = &a[1..3];
assert_eq!(slice, &[2, 3]);
```
上面的数组切片`slice`的类型是`&[i32]`,与之对比,数组的类型是`[i32;5]`,简单总结下切片的特点:
上面的数组切片 `slice` 的类型是`&[i32]`,与之对比,数组的类型是`[i32;5]`,简单总结下切片的特点:
- 切片的长度可以与数组不同,并不是固定的,而是取决于你使用时指定的起始和结束位置
- 创建切片的代价非常小,因为切片只是针对底层数组的一个引用
- 切片类型[T]拥有不固定的大小,而切片引用类型&[T]则具有固定的大小因为Rust很多时候都需要固定大小数据类型因此&[T]更有用,`&str`字符串切片也同理
- 切片类型[T]拥有不固定的大小,而切片引用类型&[T]则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此&[T]更有用,`&str`字符串切片也同理
## 总结
最后,让我们以一个综合性使用数组的例子,来结束本章节的学习:
@ -138,7 +138,7 @@ fn main() {
}
let mut sum = 0;
// 0..a.len,是一个Rust的语法糖其实就等于一个数组元素是从0,1,2一直增加到到a.len-1
// 0..a.len,是一个 Rust 的语法糖其实就等于一个数组元素是从0,1,2一直增加到到a.len-1
for i in 0..a.len() {
sum += a[i];
}
@ -153,4 +153,4 @@ fn main() {
- **在实际开发中,使用最多的是数组切片[T]**,我们往往通过引用的方式去使用`&[T]`,因为后者有固定的类型大小
至此关于数据类型部分我们已经全部学完了对于Rust学习而言我们也迈出了坚定的第一步后面将开始更高级特性的学习。未来如果大家有疑惑需要检索知识一样可以继续回顾过往的章节因为本书不仅仅是一门Rust的教程还是一本厚重的Rust工具书。
至此,关于数据类型部分,我们已经全部学完了,对于 Rust 学习而言,我们也迈出了坚定的第一步,后面将开始更高级特性的学习。未来如果大家有疑惑需要检索知识,一样可以继续回顾过往的章节,因为本书不仅仅是一门 Rust 的教程,还是一本厚重的 Rust 工具书。

@ -1,6 +1,6 @@
# 枚举
枚举(enum或enumeration)允许你通过列举可能的成员来定义一个**枚举类型**,例如扑克牌花色:
枚举(enum enumeration)允许你通过列举可能的成员来定义一个**枚举类型**,例如扑克牌花色:
```rust
enum PokerSuit {
Clubs,
@ -12,21 +12,21 @@ enum PokerSuit {
如果在此之前你没有在其它语言中使用过枚举,那么可能需要花费一些时间来理解这些概念,一旦上手,就会发现枚举的强大,甚至对它爱不释手,枚举虽好,可不要滥用哦。
再回到之前创建的`PokerSuit`,扑克总共有四种花色,而这里我们枚举出所有的可能值,这也正是`枚举`名称的由来。
再回到之前创建的 `PokerSuit`,扑克总共有四种花色,而这里我们枚举出所有的可能值,这也正是 `枚举` 名称的由来。
任何一张扑克,它的花色肯定会落在四种花色中,而且也只会落在其中一个花色上,这种特性非常适合枚举的使用,因为**枚举值**只可能是其中某一个成员。抽象来看,四种花色尽管是不同的花色,但是它们都是扑克花色这个概念,因此当某个函数处理扑克花色时,可以把它们当作相同的类型进行传参。
细心的读者应该注意到,我们对之前的`枚举类型`和`枚举值`进行了重点标注,这是因为对于新人来说容易混淆相应的概念,总而言之:
细心的读者应该注意到,我们对之前的 `枚举类型` `枚举值` 进行了重点标注,这是因为对于新人来说容易混淆相应的概念,总而言之:
**枚举类型是一个类型,它会包含所有可能的枚举成员, 而枚举值是该类型中的具体某个成员的实例。**
## 枚举值
现在来创建`PokerSuit`枚举类型的两个成员实例:
现在来创建 `PokerSuit` 枚举类型的两个成员实例:
```rust
let heart = PokerSuit::Hearts;
let diamond = PokerSuit::Diamonds;
```
我们通过`::`操作符来访问`PokerSuit`下的具体成员,从代码可以清晰看出,`heart`和`diamond`都是`PokerSuit`枚举类型的,接着可以定义一个函数来使用它们:
我们通过 `::` 操作符来访问 `PokerSuit` 下的具体成员,从代码可以清晰看出,`heart` `diamond` 都是 `PokerSuit` 枚举类型的,接着可以定义一个函数来使用它们:
```rust
fn main() {
let heart = PokerSuit::Hearts;
@ -41,7 +41,7 @@ fn print_suit(card: PokerSuit) {
}
```
`print_suit`函数的参数类型是`PokerSuit`,因此我们可以把`heart`和`diamond`传给它,虽然`heart`是基于`PokerSuit`下的`Hearts`成员实例化的,但是它是货真价实的`PokerSuit`枚举类型。
`print_suit` 函数的参数类型是 `PokerSuit`,因此我们可以把 `heart` `diamond` 传给它,虽然 `heart` 是基于 `PokerSuit` 下的 `Hearts` 成员实例化的,但是它是货真价实的 `PokerSuit` 枚举类型。
接下来,我们想让扑克牌变得更加实用,那么需要给每张牌赋予一个值:`A`(1)-`K`(13)这样再加上花色就是一张真实的扑克牌了例如红心A。
@ -70,7 +70,7 @@ fn main() {
};
}
```
这段代码很好的完成了它的使命,通过结构体`PokerCard`来代表一张牌,结构体的`suit`字段表示牌的花色,类型是`PokerSuit`枚举类型,`value`字段代表扑克牌的数值。
这段代码很好的完成了它的使命,通过结构体 `PokerCard` 来代表一张牌,结构体的 `suit` 字段表示牌的花色,类型是 `PokerSuit` 枚举类型,`value` 字段代表扑克牌的数值。
可以吗?可以!好吗?说实话,不咋地,因为还有简洁得多的方式来实现:
```rust
@ -89,7 +89,7 @@ fn main() {
直接将数据信息关联到枚举成员上,省去近一半的代码,这种实现是不是更优雅?
不仅如此,同一个枚举类型下的不同成员还能持有不同的数据类型,例如让某些花色打印`1-13`的字样,另外的花色打印上`A-K`的字样:
不仅如此,同一个枚举类型下的不同成员还能持有不同的数据类型,例如让某些花色打印 `1-13` 的字样,另外的花色打印上 `A-K` 的字样:
```rust
enum PokerCard {
Clubs(u8),
@ -107,7 +107,7 @@ fn main() {
回想一下,遇到这种不同类型的情况,再用我们之前的结构体实现方式,可行吗?也许可行,但是会复杂很多。
再来看一个来自标准库中的例子:
再来看一个来自标准库中的例子
```rust
struct Ipv4Addr {
// --snip--
@ -122,7 +122,7 @@ enum IpAddr {
V6(Ipv6Addr),
}
```
这个例子跟我们之前的扑克牌很像,只不过枚举成员包含的类型更复杂了,变成了结构体:分别通过`Ipv4Addr`和`Ipv6Addr`来定义两种不同的IP数据。
这个例子跟我们之前的扑克牌很像,只不过枚举成员包含的类型更复杂了,变成了结构体:分别通过 `Ipv4Addr` `Ipv6Addr` 来定义两种不同的IP数据。
从这些例子可以看出,**任何类型的数据都可以放入枚举成员中**: 例如字符串、数值、结构体甚至另一个枚举。
@ -142,11 +142,11 @@ fn main() {
}
```
该枚举类型代表一条消息,它包含四个不同的成员:
该枚举类型代表一条消息,它包含四个不同的成员
- `Quit` 没有任何关联数据
- `Move` 包含一个匿名结构体
- `Write` 包含一个`String`字符串
- `ChangeColor`包含三个`i32`
- `Write` 包含一个 `String` 字符串
- `ChangeColor` 包含三个 `i32`
当然,我们也可以用结构体的方式来定义这些消息:
```rust
@ -159,7 +159,7 @@ struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体
```
由于每个结构体都有自己的类型因此我们无法在需要同一类型的地方进行使用例如某个函数它的功能是接受消息并进行发送那么用枚举的方式就可以接收不同的消息但是用结构体该函数无法接受4个不同的结构体作为参数。
由于每个结构体都有自己的类型,因此我们无法在需要同一类型的地方进行使用,例如某个函数它的功能是接受消息并进行发送,那么用枚举的方式,就可以接收不同的消息,但是用结构体,该函数无法接受 4 个不同的结构体作为参数。
而且从代码规范角度来看,枚举的实现更简洁,代码内聚性更强,不像结构体的实现,分散在各个地方。
@ -167,7 +167,7 @@ struct ChangeColorMessage(i32, i32, i32); // 元组结构体
最后,再用一个实际项目中的简化片段,来结束枚举类型的语法学习。
例如我们有一个web服务,需要接受用户的长连接,假设连接有两种:`TcpStream`和`TlsStream`,但是我们希望对这两个连接的处理流程相同,也就是用同一个函数来处理这两个连接,代码如下:
例如我们有一个 WEB 服务,需要接受用户的长连接,假设连接有两种:`TcpStream` `TlsStream`,但是我们希望对这两个连接的处理流程相同,也就是用同一个函数来处理这两个连接,代码如下
```rust
func new (stream: TcpStream) {
let mut s = stream;
@ -191,15 +191,15 @@ enum Websocket {
```
## Option枚举用于处理空值
在其它编程语言中,往往都有一个`null`关键字,该关键字用于表明一个变量当前的值为空(不是零值例如整形的零值是0),也就是不存在值。当你对这些`null`进行操作时例如调用一个方法就会直接抛出null异常导致程序的崩溃因此我们在编程时需要格外的小心去处理这些`null`空值。
在其它编程语言中,往往都有一个 `null` 关键字,该关键字用于表明一个变量当前的值为空(不是零值,例如整形的零值是 0),也就是不存在值。当你对这些 `null` 进行操作时,例如调用一个方法,就会直接抛出***null异常***,导致程序的崩溃,因此我们在编程时需要格外的小心去处理这些 `null` 空值。
> Tony Hoarenull的发明者曾经说过一段非常有名的话
> Tony Hoare `null` 的发明者,曾经说过一段非常有名的话
>
> 我称之为我十亿美元的错误。当时,我在使用一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过在设计过程中,我未能抵抗住诱惑,引入了空引用的概念,因为它非常容易实现。就是因为这个决策,引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数十亿美元的苦痛和伤害。
尽管如此空值的表达依然非常有意义因为空值表示当前时刻变量的值是缺失的。有鉴于此Rust吸取了众多教训,决定抛弃`null`,而改为使用`Option`枚举变量来表述这种结果。
尽管如此空值的表达依然非常有意义因为空值表示当前时刻变量的值是缺失的。有鉴于此Rust 吸取了众多教训,决定抛弃 `null`,而改为使用 `Option` 枚举变量来表述这种结果。
`Option`枚举包含两个成员,一个成员表示含有值:`Some(T)`, 另一个表示没有值: `None`,定义如下:
`Option` 枚举包含两个成员,一个成员表示含有值`Some(T)`, 另一个表示没有值:`None`,定义如下:
```rust
enum Option<T> {
Some(T),
@ -207,9 +207,9 @@ enum Option<T> {
}
```
其中`T`是泛型参数,`Some(T)`表示该枚举成员的数据类型是`T`, 换句话说,`Some`可以包含任何类型的数据。
其中 `T` 是泛型参数,`Some(T)`表示该枚举成员的数据类型是 `T`,换句话说,`Some` 可以包含任何类型的数据。
`Option<T>` 枚举是如此有用以至于它甚至被包含在了`prelude`(prelude属于Rust标准库Rust会将最常用的类型、函数等提前引入其中避免我们再手动引入)之中,你不需要将其显式引入作用域。另外,它的成员`Some` 和 `None`也是如此,无需使用`Option::`前缀就可直接使用`Some` 和 `None`。总之,不能因为`Some(T)`和`None`中没有`Option::`的身影,就否认它们是`Option`下的卧龙凤雏。
`Option<T>` 枚举是如此有用以至于它甚至被包含在了 `prelude`(prelude属于 Rust 标准库Rust 会将最常用的类型、函数等提前引入其中,避免我们再手动引入)之中,你不需要将其显式引入作用域。另外,它的成员 `Some``None` 也是如此,无需使用 `Option::` 前缀就可直接使用 `Some``None`。总之,不能因为 `Some(T)` `None` 中没有 `Option::` 的身影,就否认它们是 `Option` 下的卧龙凤雏。
再来看以下代码:
```rust
@ -256,7 +256,7 @@ not satisfied
总的来说,为了使用 `Option<T>` 值,需要编写处理每个成员的代码。你想要一些代码只当拥有 `Some(T)` 值时运行,允许这些代码使用其中的 `T`。也希望一些代码在值为 `None` 时运行,这些代码并没有一个可用的 `T` 值。`match` 表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。
这里先简单看一下`match`的大致模样,在[模式匹配](../match-pattern/intro.md)中,我们会详细讲解:
这里先简单看一下 `match` 的大致模样,在[模式匹配](../match-pattern/intro.md)中,我们会详细讲解
```rust
fn plus_one(x: Option<i32>) -> Option<i32> {
@ -271,4 +271,4 @@ let six = plus_one(five);
let none = plus_one(None);
```
`plus_one`通过`match`来处理不同`Option`的情况。
`plus_one` 通过 `match` 来处理不同 `Option` 的情况。

@ -2,7 +2,7 @@
行百里者半五十,欢迎大家来到这里,虽然还不到中点,但是已经不远了。如果说之前学的基础数据类型是原子,那么本章将讲的数据类型可以认为是分子。
本章的重点在复合类型上,顾名思义,复合类型是由其它类型组合而成的,最典型的就是结构体`struct`和枚举`enum`。例如平面上的一个点`point(x,y)`,它由两个数值类型的值`x`和`y`组合而来。我们不想单独去维护这两个数值,因为单独一个`x`或者`y`是含义不完整的,无法标识平面上的一个点,应该把它们看作一个整体去理解和处理。
本章的重点在复合类型上,顾名思义,复合类型是由其它类型组合而成的,最典型的就是结构体 `struct` 和枚举 `enum`。例如平面上的一个点 `point(x,y)`,它由两个数值类型的值 `x` `y` 组合而来。我们不想单独去维护这两个数值,因为单独一个 `x` 或者 `y` 是含义不完整的,无法标识平面上的一个点,应该把它们看作一个整体去理解和处理。
来看一段代码,它使用我们之前学过的内容来构建文件操作:
```rust
@ -29,11 +29,11 @@ fn main() {
}
```
接下来我们的学习非常类似原型设计有的方法只提供API接口但是不提供具体实现。此外有的变量在声明之后并未使用因此在这个阶段我们需要排除一些编译器噪音Rust在编译的时候会扫描代码变量声明后未使用会以`warning`警告的形式进行提示),引入`#![allow(unused_variables)]`属性标记,该标记会告诉编译器忽略未使用的变量,不要抛出`warning`警告,具体的常见编译器属性你可以在这里查阅:[编译器属性标记](../../compiler/attributes.md).
接下来我们的学习非常类似原型设计:有的方法只提供 API 接口但是不提供具体实现。此外有的变量在声明之后并未使用因此在这个阶段我们需要排除一些编译器噪音Rust 在编译的时候会扫描代码,变量声明后未使用会以 `warning` 警告的形式进行提示),引入 `#![allow(unused_variables)]` 属性标记,该标记会告诉编译器忽略未使用的变量,不要抛出 `warning` 警告,具体的常见编译器属性你可以在这里查阅:[编译器属性标记](../../compiler/attributes.md).
`read`函数也非常有趣,它返回一个`!`,这个表明该函数是一个发散函数,不会返回任何值,包括`()`。`unimplemented!()`告诉编译器该函数尚未实现,`unimplemented!()`标记通常意味着我们期望快速完成主要代码,回头再通过搜索这些标记来完成次要代码,类似的标记还有`todo!()`,当代码执行到这种未实现的地方时,程序会直接报错: 你可以反注释`read(&mut f1, &mut vec![]);`这行,然后再观察下结果。
`read` 函数也非常有趣,它返回一个 `!`,这个表明该函数是一个发散函数,不会返回任何值,包括 `()`。`unimplemented!()` 告诉编译器该函数尚未实现,`unimplemented!()` 标记通常意味着我们期望快速完成主要代码,回头再通过搜索这些标记来完成次要代码,类似的标记还有 `todo!()`,当代码执行到这种未实现的地方时,程序会直接报错: 你可以反注释 `read(&mut f1, &mut vec![]);` 这行,然后再观察下结果。
同时,从代码设计角度来看,关于文件操作的类型和函数应该组织在一起,散落得到处都是,是难以管理和使用的。而且通过`open(&mut f1)`进行调用,也远没有使用`f1.open()`来调用好,这就体现出了只使用基本类型的局限性:**无法从更高的抽象层次去简化代码**。
同时,从代码设计角度来看,关于文件操作的类型和函数应该组织在一起,散落得到处都是,是难以管理和使用的。而且通过 `open(&mut f1)` 进行调用,也远没有使用 `f1.open()` 来调用好,这就体现出了只使用基本类型的局限性:**无法从更高的抽象层次去简化代码**。
接下来,我们将引入一个高级数据结构 - 结构体`struct`,来看看复合类型是怎样更好的解决这类问题。 开始之前先来看看Rust的重点也是难点字符串`String`和`&str`。
接下来,我们将引入一个高级数据结构 —— 结构体 `struct`,来看看复合类型是怎样更好的解决这类问题。 开始之前,先来看看 Rust 的重点也是难点:字符串 `String` `&str`

@ -1,6 +1,6 @@
# 字符串
在其他语言,字符串往往是送分题,因为实在是太简单了,例如`"hello, world"`就是字符串章节的几乎全部内容了对吧如果你带着这样的想法来学Rust我保证绝对会栽跟头**因此这一章大家一定要重视仔细阅读这里有很多其它Rust书籍中没有的内容**。
在其他语言,字符串往往是送分题,因为实在是太简单了,例如 `"hello, world"` 就是字符串章节的几乎全部内容了,对吧?如果你带着这样的想法来学 Rust我保证绝对会栽跟头**因此这一章大家一定要重视,仔细阅读,这里有很多其它 Rust 书籍中没有的内容**。
首先来看段很简单的代码:
```rust
@ -14,7 +14,7 @@ fn greet(name: String) {
}
```
`greet`函数接受一个字符串类型的`name`参数,然后打印到终端控制台中,非常好理解,你们猜猜,这段代码能否通过编译?
`greet` 函数接受一个字符串类型的 `name` 参数,然后打印到终端控制台中,非常好理解,你们猜猜,这段代码能否通过编译?
```conole
error[E0308]: mismatched types
@ -29,15 +29,15 @@ error[E0308]: mismatched types
error: aborting due to previous error
```
Bingo果然报错了编译器提示`greet`函数需要一个`String`类型的字符串,却传入了一个`&str`类型的字符串,相信读者心中现在一定有几头草泥马呼啸而过,怎么字符串也能整出这么多花活?
Bingo果然报错了编译器提示 `greet` 函数需要一个 `String` 类型的字符串,却传入了一个 `&str` 类型的字符串,相信读者心中现在一定有几头草泥马呼啸而过,怎么字符串也能整出这么多花活?
在讲解字符串之前,先来看看什么是切片?
## 切片(slice)
切片并不是Rust独有的概念在Go语言中就非常流行它允许你引用集合中部分连续的元素序列而不是引用整个集合。
切片并不是 Rust 独有的概念,在 Go 语言中就非常流行,它允许你引用集合中部分连续的元素序列,而不是引用整个集合。
对于字符串而言,切片就是对`String`类型中某一部分的引用,它看起来像这样:
对于字符串而言,切片就是对 `String` 类型中某一部分的引用,它看起来像这样:
```rust
let s = String::from("hello world");
@ -45,15 +45,15 @@ let hello = &s[0..5];
let world = &s[6..11];
```
`hello`没有引用整个`String s`,而是引用了`s`的一部分内容,通过`[0..5]`的方式来指定。
`hello` 没有引用整个 `String s`,而是引用了 `s` 的一部分内容,通过 `[0..5]` 的方式来指定。
这就是创建切片的语法,使用方括号包括的一个序列: **[开始索引..终止索引]**,其中开始索引是切片中第一个元素的索引位置,而终止索引是最后一个元素后面的索引位置,也就是这是一个`右半开区间`。在切片数据结构内部会保存开始的位置和切片的长度,其中长度是通过`终止索引` - `开始索引`的方式计算得来的。
这就是创建切片的语法,使用方括号包括的一个序列: **[开始索引..终止索引]**,其中开始索引是切片中第一个元素的索引位置,而终止索引是最后一个元素后面的索引位置,也就是这是一个 `右半开区间`。在切片数据结构内部会保存开始的位置和切片的长度,其中长度是通过 `终止索引` - `开始索引` 的方式计算得来的。
对于`let world = &s[6..11];`来说,`world`是一个切片,该切片的指针指向`s`的第7个字节(索引从0开始,6是第7个字节),且该切片的长度是`5`个字节。
对于 `let world = &s[6..11];` 来说,`world` 是一个切片,该切片的指针指向 `s` 的第 7 个字节(索引从 0 开始, 6 是第 7 个字节),且该切片的长度是 `5` 个字节。
<img alt="" src="/img/string-01.svg" class="center" style="width: 50%;" />
在使用Rust的`..`[range序列](../base-type/numbers.md#序列(Range))语法时如果你想从索引0开始可以使用如下的方式这两个是等效的
在使用 Rust `..` [range序列](../base-type/numbers.md#序列(Range))语法时,如果你想从索引 0 开始,可以使用如下的方式,这两个是等效的:
```rust
let s = String::from("hello");
@ -61,7 +61,7 @@ let slice = &s[0..2];
let slice = &s[..2];
```
同样的,如果你的切片想要包含`String`的最后一个字节,则可以这样使用:
同样的,如果你的切片想要包含 `String` 的最后一个字节,则可以这样使用:
```rust
let s = String::from("hello");
@ -71,7 +71,7 @@ let slice = &s[4..len];
let slice = &s[4..];
```
你也可以截取完整的`String`切片:
你也可以截取完整的 `String` 切片:
```rust
let s = String::from("hello");
@ -87,10 +87,10 @@ let slice = &s[..];
> let a = &s[0..2];
> println!("{}",a);
>```
>因为我们只取`s`字符串的前两个字节,但是一个中文占用三个字节,因此没有落在边界处,也就是连`中`字都取不完整,此时程序会直接崩溃退出,如果改成`&a[0..3]`,则可以正常通过编译.
> 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点, 关于该如何操作UTF8字符串参见[这里](#操作UTF8字符串)
>因为我们只取 `s` 字符串的前两个字节,但是一个中文占用三个字节,因此没有落在边界处,也就是连 `中` 字都取不完整,此时程序会直接崩溃退出,如果改成 `&a[0..3]`,则可以正常通过编译。
> 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点, 关于该如何操作 UTF8 字符串,参见[这里](#操作UTF8字符串)
字符串切片的类型标识是`&str`,因此我们可以这样声明一个函数,输入`String`类型,返回它的切片: `fn first_word(s: &String) -> &str `.
字符串切片的类型标识是 `&str`,因此我们可以这样声明一个函数,输入 `String` 类型,返回它的切片: `fn first_word(s: &String) -> &str `
有了切片就可以写出这样的安全代码:
```rust
@ -119,12 +119,12 @@ error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immuta
| ---- immutable borrow later used here
```
回忆一下借用的规则:当我们已经有了可变借用时,就无法再拥有不可变的借用。因为`clear`需要清空改变`String`,因此它需要一个可变借用,而之后的`println!`又使用了不可变借用,因此编译无法通过。
回忆一下借用的规则:当我们已经有了可变借用时,就无法再拥有不可变的借用。因为 `clear` 需要清空改变 `String`,因此它需要一个可变借用,而之后的 `println!` 又使用了不可变借用,因此编译无法通过。
从上述代码可以看出Rust不仅让我们的`API`更加容易使用,而且也在编译期就消除了大量错误!
从上述代码可以看出Rust 不仅让我们的 `API` 更加容易使用,而且也在编译期就消除了大量错误!
#### 其它切片
因为切片是对集合的部分引用,因此不仅仅字符串有切片,其它集合类型也有,例如数组:
因为切片是对集合的部分引用,因此不仅仅字符串有切片,其它集合类型也有,例如数组
```rust
let a = [1, 2, 3, 4, 5];
@ -132,7 +132,7 @@ let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
```
该数组切片的类型是`&[i32]`,数组切片和字符串切片的工作方式是一样的,例如持有一个引用指向原始数组的某个元素和长度。
该数组切片的类型是 `&[i32]`,数组切片和字符串切片的工作方式是一样的,例如持有一个引用指向原始数组的某个元素和长度。
## 字符串字面量是切片
@ -142,27 +142,27 @@ assert_eq!(slice, &[2, 3]);
let s = "Hello, world!";
```
实际上,`s`的类型是`&str`,因此你也可以这样声明:
实际上,`s` 的类型是 `&str`,因此你也可以这样声明:
```rust
let s: &str = "Hello, world!";
```
该切片指向了程序可执行文件中的某个点,这也是为什么字符串字面量是不可变的,因为`&str`是一个不可变引用。
该切片指向了程序可执行文件中的某个点,这也是为什么字符串字面量是不可变的,因为 `&str` 是一个不可变引用。
了解完切片,可以进入本节的正题了。
## 什么是字符串?
顾名思义,字符串是由字符组成的连续集合,但是在上一节中我们提到过,**Rust中的字符是Unicode类型因此每个字符占据4个字节内存空间但是在字符串中不一样字符串是UTF8编码也就是字符所占的字节数是变化的(1-4)**,这样有助于大幅降低字符串所占用的内存空间.
顾名思义,字符串是由字符组成的连续集合,但是在上一节中我们提到过,**Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF8 编码,也就是字符所占的字节数是变化的(1 - 4)**,这样有助于大幅降低字符串所占用的内存空间
Rust在语言级别只有一种字符串类型`str`,它通常是以引用类型出现`&str`,也就是上文提到的字符串切片。虽然语言级别只有上述的`str`类型,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是`String`类型。
Rust 在语言级别,只有一种字符串类型: `str`,它通常是以引用类型出现 `&str`,也就是上文提到的字符串切片。虽然语言级别只有上述的 `str` 类型,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是 `String` 类型。
`str`类型是硬编码进可执行文件,也无法被修改,但是`String`则是一个可增长、可改变且具有所有权的UTF8编码字符串**当Rust用户提到字符串时往往指的就是`String`类型和`&str`字符串切片类型这两个类型都是UTF8编码**.
`str` 类型是硬编码进可执行文件,也无法被修改,但是 `String` 则是一个可增长、可改变且具有所有权的 UTF8 编码字符串,**当 Rust 用户提到字符串时,往往指的就是 `String` 类型和 `&str` 字符串切片类型,这两个类型都是 UTF8 编码**。
除了`String`类型的字符串Rust的标准库还提供了其他类型的字符串例如`OsString`,`OsStr`,`CsString`和`CsStr`等,注意到这些名字都以`String`或者`Str`结尾了吗?它们分别对应的是具有所有权和被借用的变量。
除了 `String` 类型的字符串Rust 的标准库还提供了其他类型的字符串,例如 `OsString` `OsStr` `CsString` 和` CsStr` 等,注意到这些名字都以 `String` 或者 `Str` 结尾了吗?它们分别对应的是具有所有权和被借用的变量。
#### 操作字符串
由于String是可变字符串因此我们可以对它进行创建、增删操作下面的代码汇总了相关的操作方式
由于 `String` 是可变字符串,因此我们可以对它进行创建、增删操作,下面的代码汇总了相关的操作方式:
```rust
fn main() {
// 创建一个空String
@ -199,12 +199,12 @@ fn main() {
}
```
在上面代码中,有一处需要解释的地方,就是使用`+`来对字符串进行相加操作, 这里之所以使用`s1 + &s2`的形式,是因为`+`使用了`add`方法,该方法的定义类似:
在上面代码中,有一处需要解释的地方,就是使用 `+` 来对字符串进行相加操作, 这里之所以使用 `s1 + &s2` 的形式,是因为 `+` 使用了 `add` 方法,该方法的定义类似:
```rust
fn add(self, s: &str) -> String {
```
因为该方法涉及到更复杂的特征功能,因此我们这里简单说明下,`self`是`String`类型的字符串`s1`,该函数说明,只能将`&str`类型的字符串切片添加到`String`类型的`s1`上,然后返回一个新的`String`类型,所以`let s3 = s1 + &s2;`就很好解释了,将`String`类型的`s1`与`&str`类型的`s2`进行相加,最终得到`String`类型的s3.
因为该方法涉及到更复杂的特征功能,因此我们这里简单说明下, `self``String` 类型的字符串 `s1`,该函数说明,只能将 `&str` 类型的字符串切片添加到 `String` 类型的 `s1` 上,然后返回一个新的 `String` 类型,所以 `let s3 = s1 + &s2;` 就很好解释了,将 `String` 类型的 `s1``&str` 类型的 `s2` 进行相加,最终得到 `String` 类型的 `s3`
由此可推,以下代码也是合法的:
```rust
@ -216,17 +216,17 @@ let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;
```
`String` + `&str`返回一个`String`,然后再继续跟一个`&str`进行`+`操作,返回一个`String`类型,不断循环,最终生成一个`s`,也是`String`类型。
`String + &str`返回一个 `String`,然后再继续跟一个 `&str` 进行 `+` 操作,返回一个 `String` 类型,不断循环,最终生成一个 `s`,也是 `String` 类型。
在上面代码中,我们做了一个有些难以理解的`&String`操作,下面来展开讲讲。
在上面代码中,我们做了一个有些难以理解的 `&String` 操作,下面来展开讲讲。
## String与&str的转换
在之前的代码中,已经见到好几种从`&str`类型生成`String`类型的操作:
在之前的代码中,已经见到好几种从 `&str` 类型生成 `String` 类型的操作:
- `String::from("hello,world")`
- `"hello,world".to_string()`
那么如何将`String`类型转为`&str`类型呢?答案很简单,取引用即可:
那么如何将 `String` 类型转为 `&str` 类型呢?答案很简单,取引用即可:
```rust
fn main() {
let s = String::from("hello,world!");
@ -240,11 +240,11 @@ fn say_hello(s: &str) {
}
```
实际上这种灵活用法是因为`deref`隐式强制转换,具体我们会在[Deref特征](../../traits/deref.md)进行详细讲解。
实际上这种灵活用法是因为 `deref` 隐式强制转换,具体我们会在 [Deref特征](../../traits/deref.md)进行详细讲解。
## 字符串索引
在其它语言中使用索引的方式访问字符串的某个字符或者子串是很正常的行为但是在Rust中就会报错:
在其它语言中,使用索引的方式访问字符串的某个字符或者子串是很正常的行为,但是在 Rust 中就会报错:
```rust
let s1 = String::from("hello");
let h = s1[0];
@ -259,15 +259,14 @@ fn say_hello(s: &str) {
```
#### 深入字符串内部
字符串的底层的数据存储格式实际上是[u8],一个字节数组。对于`let hello = String::from("Hola");`这行代码来说,`hello`的长度是`4`个字节,因为`"hola"`中的每个字母在UTF8编码中仅占用1个字节但是对于下面的代码呢?
字符串的底层的数据存储格式实际上是[ `u8` ],一个字节数组。对于 `let hello = String::from("Hola");` 这行代码来说, `hello` 的长度是 `4` 个字节,因为 `"hola"` 中的每个字母在 UTF8 编码中仅占用 1 个字节,但是对于下面的代码呢?
```rust
let hello = String::from("中国人");
```
如果问你该字符串多长,你可能会说`3`,但是实际上是`9`个字节的长度因为每个汉字在UTF8中的长度是`3`个字节,因此这种情况下对`hello`进行索引
访问`&hello[0]`没有任何意义,因为你取不到`中`这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的返回值。
如果问你该字符串多长,你可能会说 `3`,但是实际上是 `9` 个字节的长度,因为每个汉字在 UTF8 中的长度是 `3` 个字节,因此这种情况下对 `hello` 进行索引,访问 `&hello[0]` 没有任何意义,因为你取不到 `中` 这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的返回值。
#### 字符串的不同表现形式
现在看一下用梵文写的字符串`“नमस्ते”`, 它底层的字节数组如下形式:
现在看一下用梵文写的字符串 `“नमस्ते”`, 它底层的字节数组如下形式:
```rust
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
@ -281,12 +280,12 @@ let hello = String::from("中国人");
["न", "म", "स्", "ते"]
```
所以可以看出来Rust提供了不同的字符串展现方式这样程序可以挑选自己想要的方式去使用而无需去管字符串从人类语言角度看长什么样。
所以,可以看出来 Rust 提供了不同的字符串展现方式,这样程序可以挑选自己想要的方式去使用,而无需去管字符串从人类语言角度看长什么样。
还有一个原因导致了Rust不允许去索引字符因为索引操作我们总是期望它的性能表现是O(1),然而对于`String`类型来说无法保证这一点因为Rust可能需要从0开始去遍历字符串来定位合法的字符。
还有一个原因导致了 Rust 不允许去索引字符:因为索引操作,我们总是期望它的性能表现是 O(1),然而对于 `String` 类型来说,无法保证这一点,因为 Rust 可能需要从 0 开始去遍历字符串来定位合法的字符。
## 字符串切片
前文提到过字符串切片是非常危险的操作因为切片的索引是通过字节来进行但是字符串又是UTF8编码因此你无法保证索引的字节刚好落在字符的边界上例如
前文提到过,字符串切片是非常危险的操作,因为切片的索引是通过字节来进行,但是字符串又是 UTF8 编码,因此你无法保证索引的字节刚好落在字符的边界上,例如:
```rust
let hello = "中国人";
@ -297,15 +296,15 @@ let s = &hello[0..2];
thread 'main' panicked at 'byte index 2 is not a char boundary; it is inside '中' (bytes 0..3) of `中国人`', src/main.rs:4:14
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
这里提示的很清楚,我们索引的字节落在了`中`字符的内部,这种返回没有任何意义。
这里提示的很清楚,我们索引的字节落在了 `中` 字符的内部,这种返回没有任何意义。
因此在通过索引区间来访问字符串时,需要格外的小心,一不注意,就会导致你程序的崩溃!
因此在通过索引区间来访问字符串时,**需要格外的小心**,一不注意,就会导致你程序的崩溃!
## 操作UTF8字符串
前文提到了几种使用UTF8字符串的方式下面来一一说明。
前文提到了几种使用 UTF8 字符串的方式,下面来一一说明。
#### 字符
如果你想要以Unicode字符的方式遍历字符串最好的办法是使用`chars`方法,例如:
如果你想要以 Unicode 字符的方式遍历字符串,最好的办法是使用 `chars` 方法,例如:
```rust
for c in "中国人".chars() {
println!("{}", c);
@ -339,28 +338,28 @@ for b in "中国人".bytes() {
```
#### 获取子串
想要准确的从UTF8字符串中获取子串是较为复杂的事情例如想要从`holla中国人नमस्ते`这种变长的字符串中取出某一个子串,使用标准库你是做不到的
你需要在`crates.io`上搜索`utf8`来寻找想要的功能。
想要准确的从UTF8字符串中获取子串是较为复杂的事情例如想要从 `holla中国人नमस्ते` 这种变长的字符串中取出某一个子串,使用标准库你是做不到的
你需要在 `crates.io` 上搜索 `utf8` 来寻找想要的功能。
可以考虑尝试下这个库:[utf8_slice](https://crates.io/crates/utf8_slice).
可以考虑尝试下这个库:[utf8_slice](https://crates.io/crates/utf8_slice)
## 字符串深度剖析
那么问题来了,为啥`String`可变,而字符串字面值`str`却不可以?
那么问题来了,为啥 `String` 可变,而字符串字面值 `str` 却不可以?
就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串的不可变性。不幸的是,我们不能为了获得这种性能,而把每一个在编译时大小未知的文本都放进内存中(你也做不到!),因为有的字符串是在程序运行得过程中动态生成的。
就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串的不可变性。不幸的是,我们不能为了获得这种性能,而把每一个在编译时大小未知的文本都放进内存中(你也做不到!),因为有的字符串是在程序运行得过程中动态生成的。
对于 `String` 类型,为了支持一个可变、可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容,这些都是在程序运行时完成的:
- 首先向操作系统请求内存来存放`String`对象
- 首先向操作系统请求内存来存放 `String` 对象
- 在使用完成后,将内存释放,归还给操作系统
其中第一个由`String::from`完成它创建了一个全新的String.
其中第一个由 `String::from` 完成,它创建了一个全新的 `String`
重点来了,到了第二部分,就是百家齐放的环节,在有**垃圾回收GC**的语言中GC来负责标记并清除这些不再使用的内存对象这个过程都是自动完成无需开发者关心非常简单好用但是在无GC的语言中需要开发者手动去释放这些内存对象就像创建对象需要通过编写代码来完成一样未能正确释放对象造成的后果简直不可估量.
重点来了,到了第二部分,就是百家齐放的环节,在有**垃圾回收 GC** 的语言中GC 来负责标记并清除这些不再使用的内存对象,这个过程都是自动完成,无需开发者关心,非常简单好用;但是在无 GC 的语言中,需要开发者手动去释放这些内存对象,就像创建对象需要通过编写代码来完成一样,未能正确释放对象造成的后果简直不可估量
对于Rust而言安全和性能是写到骨子里的核心特性如果使用GC那么会牺牲性能如果使用手动管理内存那么会牺牲安全这该怎么办为此Rust的开发者想出了一个无比惊艳的办法变量在离开作用域后就自动释放其占用的内存:
对于 Rust 而言,安全和性能是写到骨子里的核心特性,如果使用 GC那么会牺牲性能如果使用手动管理内存那么会牺牲安全这该怎么办为此Rust 的开发者想出了一个无比惊艳的办法:变量在离开作用域后,就自动释放其占用的内存
```rust
{
@ -371,7 +370,7 @@ for b in "中国人".bytes() {
// s 不再有效,内存被释放
```
与其它系统编程语言的`free`函数相同Rust也提供了一个释放内存的函数:`drop`,但是不同的是,其它语言要手动调用`free`来释放每一个变量占用的内存而Rust则在变量离开作用域时自动调用`drop`函数: 上面代码中Rust 在结尾的 `}` 处自动调用 `drop`
与其它系统编程语言的 `free` 函数相同Rust 也提供了一个释放内存的函数: `drop`,但是不同的是,其它语言要手动调用 `free` 来释放每一个变量占用的内存,而 Rust 则在变量离开作用域时,自动调用 `drop` 函数: 上面代码中Rust 在结尾的 `}` 处自动调用 `drop`
> 其实,在 C++ 中,也有这种概念: *Resource Acquisition Is Initialization (RAII)*。如果你使用过 RAII 模式的话应该对 Rust 的 `drop` 函数并不陌生

@ -1,6 +1,6 @@
# 结构体
上一节中提到需要一个更高级的数据结构来帮助我们更好的抽象问题,结构体`struct`恰恰就是这样的复合数据结构,它是由其它数据类型组合而来。 其它语言也有类似的数据结构,不过可能有不同的名称,例如`object`、`record`等。
上一节中提到需要一个更高级的数据结构来帮助我们更好的抽象问题,结构体 `struct` 恰恰就是这样的复合数据结构,它是由其它数据类型组合而来。 其它语言也有类似的数据结构,不过可能有不同的名称,例如 `object` `record` 等。
结构体跟之前讲过的[元组](./tuple.md)有些相像:都是由多种类型组合而成。但是与元组不同的是,结构体可以为内部的每个字段起一个富有含义的名称。因此结构体更加灵活更加强大,你无需依赖这些字段的顺序来访问和解析它们。
@ -9,9 +9,9 @@
#### 定义结构体
一个结构体有几部分组成:
- 通过关键字`struct`定义
- 一个清晰明确的结构体`名称`
- 几个有名字的结构体`字段`
- 通过关键字 `struct` 定义
- 一个清晰明确的结构体 `名称`
- 几个有名字的结构体 `字段`
例如以下结构体定义了某网站的用户:
```rust
@ -22,10 +22,10 @@ struct User {
sign_in_count: u64,
}
```
该结构体名称是`User`拥有4个字段且每个字段都有对应的字段名及类型声明例如`username`代表了用户名,是一个可变的`String`类型。
该结构体名称是 `User`,拥有 4 个字段,且每个字段都有对应的字段名及类型声明,例如 `username` 代表了用户名,是一个可变的 `String` 类型。
#### 创建结构体实例
为了使用上述结构体,我们需要创建`User`结构体的**实例**
为了使用上述结构体,我们需要创建 `User` 结构体的**实例**
```rust
let user1 = User {
email: String::from("someone@example.com"),
@ -37,10 +37,10 @@ struct User {
有几点值得注意:
1. 初始化实例时,**每个字段**都需要进行初始化
2. 初始化时的字段顺序不需要和结构体定义时的顺序一致
2. 初始化时的字段顺序**不需要**和结构体定义时的顺序一致
#### 访问结构体字段
通过`.`操作符即可访问结构体实例内部的字段值,也可以修改它们:
通过 `.` 操作符即可访问结构体实例内部的字段值,也可以修改它们:
```rust
let mut user1 = User {
email: String::from("someone@example.com"),
@ -51,10 +51,10 @@ struct User {
user1.email = String::from("anotheremail@example.com");
```
需要注意的是必须要将结构体实例声明为可变的才能修改其中的字段Rust不支持将某个结构体某个字段标记为可变.
需要注意的是必须要将结构体实例声明为可变的才能修改其中的字段Rust 不支持将某个结构体某个字段标记为可变
#### 简化结构体创建
下面的函数类似一个构建函数,返回了`User`结构体的实例:
下面的函数类似一个构建函数,返回了 `User` 结构体的实例:
```rust
fn build_user(email: String, username: String) -> User {
User {
@ -65,7 +65,7 @@ fn build_user(email: String, username: String) -> User {
}
}
```
它接收两个字符串参数:`email`和`username`,然后使用它们来创建一个`User`结构体,并且返回。可以注意到这两行:`email: email`和`username: username`,非常的扎眼,因为实在有些啰嗦,如果你从typescript过来肯定会鄙视Rust一番不过好在它也不是无可救药:
它接收两个字符串参数: `email` `username`,然后使用它们来创建一个 `User` 结构体,并且返回。可以注意到这两行: `email: email` `username: username`,非常的扎眼,因为实在有些啰嗦,如果你从 TypeScript 过来,肯定会鄙视 Rust 一番,不过好在,它也不是无可救药:
```rust
fn build_user(email: String, username: String) -> User {
User {
@ -76,11 +76,11 @@ fn build_user(email: String, username: String) -> User {
}
}
```
如上所示,当函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化,跟`typescript`中一模一样.
如上所示,当函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化,跟 TypeScript 中一模一样。
#### 结构体更新语法
在实际场景中,有一种情况很常见:根据已有的结构体实例,创建新的结构体实例,例如根据已有的`user1`实例来构建`user2`
在实际场景中,有一种情况很常见:根据已有的结构体实例,创建新的结构体实例,例如根据已有的 `user1` 实例来构建 `user2`
```rust
let user2 = User {
active: user1.active,
@ -90,25 +90,25 @@ fn build_user(email: String, username: String) -> User {
};
```
老话重提,如果你从typescript过来肯定觉得啰嗦爆了竟然手动把`user1`的三个字段逐个赋值给`user2`好在Rust为我们提供了`结构体更新语法`:
老话重提,如果你从 TypeScript 过来,肯定觉得啰嗦爆了:竟然手动把 `user1` 的三个字段逐个赋值给 `user2`,好在 Rust 为我们提供了 `结构体更新语法`
```rust
let user2 = User {
email: String::from("another@example.com"),
..user1
};
```
因为`user2`仅仅在`email`上与`user1`不同,因此我们只需要对`email`进行赋值,剩下的通过结构体更新语法`..user1`即可完成。
因为 `user2` 仅仅在 `email` 上与 `user1` 不同,因此我们只需要对 `email` 进行赋值,剩下的通过结构体更新语法 `..user1` 即可完成。
`..`语法表明凡是我们没有显示声明的字段,全部从`user1`中自动获取。需要注意的是`..user1`必须在结构体的尾部使用。
`..` 语法表明凡是我们没有显示声明的字段,全部从 `user1` 中自动获取。需要注意的是 `..user1` 必须在结构体的尾部使用。
> 结构体更新语法跟赋值语句`=`非常相像,因此在上面代码中,`user1`的部分字段所有权被转移到`user2`中:`username`字段发生了所有权转移,作为结果,`user1`无法再被使用。
> 结构体更新语法跟赋值语句 `=` 非常相像,因此在上面代码中,`user1` 的部分字段所有权被转移到 `user2` 中:`username` 字段发生了所有权转移,作为结果,`user1` 无法再被使用。
>
> 聪明的读者肯定要发问了:明明有三个字段进行了自动赋值,为何只有`username`发生了所有权转移?
> 聪明的读者肯定要发问了:明明有三个字段进行了自动赋值,为何只有 `username` 发生了所有权转移?
>
> 仔细回想一下[所有权](../ownership/ownership.md#拷贝(浅拷贝))那一节的内容我们提到了Copy特征实现了Copy特征的类型无需所有权转移可以直接在赋值时进行
> 数据拷贝,其中`bool`和`u64`类型就实现了`Copy`特征,因此`active`和`sign_in_count`字段在赋值给user2时仅仅发生了拷贝而不是所有权转移.
> 仔细回想一下[所有权](../ownership/ownership.md#拷贝(浅拷贝))那一节的内容,我们提到了 `Copy` 特征:实现了 `Copy` 特征的类型无需所有权转移,可以直接在赋值时进行
> 数据拷贝,其中 `bool` `u64` 类型就实现了 `Copy` 特征,因此 `active` `sign_in_count` 字段在赋值给 `user2` 时,仅仅发生了拷贝,而不是所有权转移
>
> 值得注意的是:`username`所有权被转移给了`user2`,导致了`user1`无法再被使用,但是并不代表`user1`内部的其它字段不能被继续使用,例如:
> 值得注意的是:`username` 所有权被转移给了 `user2`,导致了 `user1` 无法再被使用,但是并不代表 `user1` 内部的其它字段不能被继续使用,例如:
```rust
# #[derive(Debug)]
@ -161,12 +161,12 @@ println!("{:?}", user1);
}
```
上面定义的`File`结构体在内存中的排列如下图所示:
上面定义的 `File` 结构体在内存中的排列如下图所示:
<img alt="" src="/img/struct-01.png" class="center" />
从图中可以清晰的看出`File`结构体两个字段`name`和`data`分别拥有底层两个`[u8]`数组的所有权(`String`类型的底层也是`[u8]`数组),通过`ptr`指针指向底层数组的内存地址,这里你可以把`ptr`指针理解为Rust中的引用类型。
从图中可以清晰的看出 `File` 结构体两个字段 `name``data` 分别拥有底层两个 `[u8]` 数组的所有权(`String` 类型的底层也是 `[u8]` 数组),通过 `ptr` 指针指向底层数组的内存地址,这里你可以把 `ptr` 指针理解为 Rust 中的引用类型。
该图片也侧面印证了:**把结构体中具有所有权的字段转移出去后,将无法再访问该字段,但是可以正常访问其它的字段**.
该图片也侧面印证了:**把结构体中具有所有权的字段转移出去后,将无法再访问该字段,但是可以正常访问其它的字段**
## 元组结构体(Tuple Struct)
@ -179,18 +179,18 @@ println!("{:?}", user1);
let origin = Point(0, 0, 0);
```
元组结构体在你希望有一个整体名称,但是又不关心里面字段的名称时将非常有用。例如上面的`Point`元组结构体众所周知3D点是`(x,y,z)`形式的坐标点,因此我们无需再为内部的字段逐一命名为:`x`,`y`,`z`。
元组结构体在你希望有一个整体名称,但是又不关心里面字段的名称时将非常有用。例如上面的 `Point` 元组结构体众所周知3D点是 `(x, y, z)` 形式的坐标点,因此我们无需再为内部的字段逐一命名为:`x`, `y`, `z`
## 元结构体(Unit-like Struct)
还记得之前讲过的基本没啥用的[单元类型](../base-type/char-bool.md#单元类型)吧? 元结构体就跟它很像,没有任何字段和属性,但是好在,它还挺有用。
还记得之前讲过的基本没啥用的[单元类型](../base-type/char-bool.md#单元类型)吧元结构体就跟它很像,没有任何字段和属性,但是好在,它还挺有用。
如果你定义一个类型,但是不关心该类型的内容, 只关心它的行为时,就可以使用`元结构体`:
如果你定义一个类型,但是不关心该类型的内容, 只关心它的行为时,就可以使用 `元结构体`
```rust
struct AlwaysEqual;
let subject = AlwaysEqual;
// 我们不关心为AlwaysEqual的字段数据只关心它的行为因此将它声明为元结构体然后再为它实现某个特征
// 我们不关心为 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为元结构体,然后再为它实现某个特征
impl SomeTrait for AlwaysEqual {
}
@ -199,9 +199,9 @@ impl SomeTrait for AlwaysEqual {
## 结构体数据的所有权
在之前的`User` 结构体的定义中,有一处细节:我们使用了自身拥有所有权的 `String` 类型而不是基于引用的`&str` 字符串切片类型。这是一个有意而为之的选择因为我们想要这个结构体拥有它所有的数据,而不是从其它地方借用数据。
在之前的 `User` 结构体的定义中,有一处细节:我们使用了自身拥有所有权的 `String` 类型而不是基于引用的 `&str` 字符串切片类型。这是一个有意而为之的选择因为我们想要这个结构体拥有它所有的数据,而不是从其它地方借用数据。
你也可以让`User`结构体从其它对象借用数据,不过这么做,就需要引入**生命周期**这个新概念(也是一个复杂的概念),简而言之,生命周期能确保结构体的作用范围要比它所借用的数据的作用范围要小。
你也可以让 `User` 结构体从其它对象借用数据,不过这么做,就需要引入[生命周期lifetimes](../../advance/lifetime/basic.md)这个新概念(也是一个复杂的概念),简而言之,生命周期能确保结构体的作用范围要比它所借用的数据的作用范围要小。
总之,如果你想在结构体中使用一个引用,就必须加上生命周期,否则就会报错:

@ -9,9 +9,9 @@ fn main() {
}
```
变量`tup`被绑定了一个元组值`(500, 6.4, 1)`,该元组的类型是`(i32, f64, u8)`,看到没?元组是用括号将多个类型组合到一起,简单吧?
变量 `tup` 被绑定了一个元组值 `(500, 6.4, 1)`,该元组的类型是 `(i32, f64, u8)`,看到没?元组是用括号将多个类型组合到一起,简单吧?
可以使用模式匹配或者`.`操作符来获取元组中的值。
可以使用模式匹配或者 `.` 操作符来获取元组中的值。
### 用模式匹配解构元组
@ -25,11 +25,11 @@ fn main() {
}
```
上述代码首先创建一个元组,然后将其绑定到`tup`上,接着使用`let (x, y, z) = tup;`来完成一次模式匹配,因为元组是`(n1,n2,n3)`形式的,因此我们用一模一样的`(x,y,z)`形式来进行匹配,元组中对应的值会绑定到变量`x``y``z`上。这就是解构:用同样的形式把一个复杂对象中的值匹配出来。
上述代码首先创建一个元组,然后将其绑定到 `tup` 上,接着使用 `let (x, y, z) = tup;` 来完成一次模式匹配,因为元组是 `(n1, n2, n3)` 形式的,因此我们用一模一样的 `(x, y, z)` 形式来进行匹配,元组中对应的值会绑定到变量 `x` `y` `z`上。这就是解构:用同样的形式把一个复杂对象中的值匹配出来。
### 用`.`来访问元组
模式匹配可以让我们一次性把元组中的值全部或者部分获取出来如果只想要访问某个特定元素那模式匹配就略显繁琐对此Rust提供了`.`的访问方式:
模式匹配可以让我们一次性把元组中的值全部或者部分获取出来如果只想要访问某个特定元素那模式匹配就略显繁琐对此Rust 提供了 `.` 的访问方式:
```rust
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
@ -41,10 +41,10 @@ fn main() {
let one = x.2;
}
```
和其它语言的数组、字符串一样元组的索引从0开始。
和其它语言的数组、字符串一样,元组的索引从 0 开始。
### 元组的使用示例
元组在函数返回值场景很常用,例如下面的代码,可以使用元组返回多个值:
元组在函数返回值场景很常用,例如下面的代码,可以使用元组返回多个值
```rust
fn main() {
@ -62,8 +62,8 @@ fn calculate_length(s: String) -> (String, usize) {
}
```
`calculate_length`函数接收`s1`字符串的所有权,然后计算字符串的长度,接着把字符串所有权和字符串长度再返回给`s2`和`len`变量。
`calculate_length` 函数接收 `s1` 字符串的所有权,然后计算字符串的长度,接着把字符串所有权和字符串长度再返回给 `s2` `len` 变量。
在其他语言中,可以用结构体来声明一个三维空间中的点,例如`Point(10,20,30)`虽然使用Rust元组也可以做到`(10,20,30)`,但是这样写有个非常重大的缺陷:
在其他语言中,可以用结构体来声明一个三维空间中的点,例如 `Point(10, 20, 30)`,虽然使用 Rust 元组也可以做到:`(10, 20, 30)`,但是这样写有个非常重大的缺陷:
**不具备任何清晰的含义**,在下一章节中,会提到一种与元组类似的结构体,`元组结构体`,可以解决这个问题。
**不具备任何清晰的含义**,在下一章节中,会提到一种与元组类似的结构体, `元组结构体`,可以解决这个问题。

@ -1,39 +1,39 @@
# 避免从入门到放弃
很多人都在学Rust ing也有很多人在放弃 ing。想要顺利学完 Rust大家需要谨记本文列出的内容否则这极有可能是又又又一次从入门到放弃之旅。
很多人都在学 Rust ing也有很多人在放弃 ing。想要顺利学完 Rust大家需要谨记本文列出的内容否则这极有可能是又又又一次从入门到放弃之旅。
Rust 是一门全新的语言,它会带给你前所未有的体验,提升你的通用编程水平,甚至于赋予你全新的编程思想。在此时此刻,大家可能还半信半疑,但是当学完它再回头看时,可能你也会认同这些貌似浮夸的赞美。
## 避免试一试的心态
在学习Go、Python等编程语言时你可能会一边工作一边轻松愉快的学习它们但是Rust不行。原因如文章开头所说在学习Rust的同时你会收获很多语言之外的知识因此 Rust 在入门阶段比很多编程语言要更难,但是一旦入门,你将收获一个全新的自己,成为一个更加优秀的程序员。
在学习 Go、Python 等编程语言时,你可能会一边工作,一边轻松愉快的学习它们,但是 Rust 不行。原因如文章开头所说,在学习 Rust 的同时你会收获很多语言之外的知识,因此 Rust 在入门阶段比很多编程语言要更难,但是一旦入门,你将收获一个全新的自己,成为一个更加优秀的程序员。
在学习过程中一开始可能会轻松愉快但是在开始Rust核心概念时(所有权、借用、生命周期、智能指针等),难度可能会陡然提升,此时就需要认真对待起来,否则会为后面埋下很多难以填补的坑,结果最后你可能只有两个选择:重新学一遍 or 放弃。
在学习过程中,一开始可能会轻松愉快,但是在开始 Rust 核心概念时(所有权、借用、生命周期、智能指针等),难度可能会陡然提升,此时就需要认真对待起来,否则会为后面埋下很多难以填补的坑,结果最后你可能只有两个选择:重新学一遍 or 放弃。
因此,在学习过程中,给大家三点建议:
- 要提前做好会遇到困难的准备因为如上面所说学习Rust不仅仅是在学习一门编程语言
- 不要抱着试一试的心态去试一试否则是浪费时间和消耗学习的激情作为连续六年全世界最受喜欢的语言Rust不仅仅是值得试一试 :)
- 要提前做好会遇到困难的准备,因为如上面所说,学习 Rust 不仅仅是在学习一门编程语言
- 不要抱着试一试的心态去试一试否则是浪费时间和消耗学习的激情作为连续六年全世界最受喜欢的语言Rust 不仅仅是值得试一试 :)
- 深入学习一本好书或教程
总之, Rust 入门难,但是在你一次次克服艰难险阻的同时,也一次次收获了与众不同的编程经验,最后历经九九八十一难,立地成大佬。 给自己一个机会,也给 Rust 一个机会
## 深入学习一本好书
Rust跟其它语言不一样你无法看了一遍语法然后就能上手写代码我说的就是对比 Go 语言,后者的简单易用是有目共睹的。
Rust 跟其它语言不一样,你无法看了一遍语法,然后就能上手写代码,对,我说的就是对比 Go 语言,后者的简单易用是有目共睹的。
这些年,我遇到过太多在网上看了一遍菜鸟教程(或其它简易教程)就上手写demo甚至项目的同学无一例外都各种碰壁、趟坑最后要么放弃要么回炉重造之前的时间和精力基本等同浪费。
这些年,我遇到过太多在网上看了一遍菜鸟教程(或其它简易教程)就上手写 demo 甚至项目的同学,无一例外,都各种碰壁、趟坑,最后要么放弃,要么回炉重造,之前的时间和精力基本等同浪费。
因此大家一定要舍得投入时间沉下心去读一本好书这本书会带你深入浅出地学习使用Rust所需的各种知识还会带你提前趟坑这些坑往往是需要大量的时间才能领悟的。
因此,大家一定要舍得投入时间,沉下心去读一本好书,这本书会带你深入浅出地学习使用 Rust 所需的各种知识,还会带你提前趟坑,这些坑往往是需要大量的时间才能领悟的。
在以前我可能会推荐看官方那本书的英文原版 + async book + nomicon这几本书的组合但是现在有了一本更适合中国用户的书籍那就是 [<<Rust语言圣经>>](https://github.com/sunface/rust-course),内容好坏大家一读即知,光就文字而言,那绝对是行云流水般的阅读体验,可以极大提升你的学习效率,也不再因为反复读也读不懂一句话而烦闷不堪。
在以前我可能会推荐看官方那本书的英文原版 + async book + nomicon 这几本书的组合,但是现在有了一本更适合中国用户的书籍,那就是 [<<Rust语言圣经>>](https://github.com/sunface/rust-course),内容好坏大家一读即知,光就文字而言,那绝对是行云流水般的阅读体验,可以极大提升你的学习效率,也不再因为反复读也读不懂一句话而烦闷不堪。
## 千万别从链表或图开始练手
CS课程中我们会学习大量的常用数据结构和算法因此大家都养成了一种好习惯学习一门新语言先用它写个链表或图试试。
CS 课程中我们会学习大量的常用数据结构和算法,因此大家都养成了一种好习惯:学习一门新语言,先用它写个链表或图试试。
我的天在Rust中千万别这么干你是在扼杀自己之前的努力因为不像其它语言链表在Rust中简直是地狱一般的难度我见过太多英雄好汉难过链表关最终黯然退幕。我不希望正在阅读此文的你也成为其中一个 :
我的天,在 Rust **千万别这么干**,你是在扼杀自己之前的努力!因为不像其它语言,链表在 Rust 中简直是地狱一般的难度,我见过太多英雄好汉难过链表关,最终黯然退幕。我不希望正在阅读此文的你也成为其中一个 :
这些自引用类型的数据结构(包含了字段,该字段又引用了自身),它们是恶魔,它们不仅仅在蹂躏着新手,还在折磨着老手,有意思的是,它们的难恰恰是Rust的优点导致的无gc也无手动内存管理,内存安全。
这些自引用类型的数据结构(包含了字段,该字段又引用了自身),它们是恶魔,它们不仅仅在蹂躏着新手,还在折磨着老手,有意思的是,它们的难恰恰是 Rust 的优点导致的:无 GC 也无手动内存管理,内存安全。
这两点的实现并不是凭空产生的而是通过Rust一套非常强大、优美的机制提供了支持这些机制一旦你学到就会被它巧妙的构思和设计而征服进而被 Rust 深深吸引!但是一切选择都有利弊,这种机制的弊端就在于实现链表这种数据结构时,会变得非常非常复杂。
这两点的实现并不是凭空产生的,而是通过 Rust 一套非常强大、优美的机制提供了支持,这些机制一旦你学到,就会被它巧妙的构思和设计而征服,进而被 Rust 深深吸引!但是一切选择都有利弊,这种机制的弊端就在于实现链表这种数据结构时,会变得非常非常复杂。
你需要糅合各种知识才能解决这个问题但是这显然不是一个新手应该独自去面对的。总之不会链表对于Rust的学习和写项目真的没有任何影响直接使用大神已经写好的数据结构包就可以。
你需要糅合各种知识,才能解决这个问题,但是这显然不是一个新手应该独自去面对的。总之,不会链表对于 Rust 的学习和写项目,真的没有任何影响,直接使用大神已经写好的数据结构包就可以。
如果想要练手,我们可以换个方向开始,当然如果你就是喜欢征服困难,那没问题,就从链表开始。但是无论选择哪个,之前提到的那本书都会给你莫大的帮助,包括如何实现一个链表!
@ -46,10 +46,10 @@ CS课程中我们会学习大量的常用数据结构和算法因此大家都
同时也不要忽略编译器给出的警告信息(warnings),因为里面包含了 `cargo clippy` 给出的 `lint` 提示,这些提示不仅仅包含代码风格,甚至包含了一些隐藏很深的错误!至于这些错误为何不是 `error` 形式出现,随着学习的深入,你将逐渐理解 Rust 的各种设计选择,包括这个问题。
## 不要强制自己使用其它编程语言的最佳实践来写Rust
大多数其它编程语言适用的最佳实践在Rust中也可以很好的使用但是 Rust 并不是一门专门的面向对象或者函数式语言,因此在使用自己喜欢的编程风格时,也要考虑遵循 Rust 应有的实践。
## 不要强制自己使用其它编程语言的最佳实践来写 Rust
大多数其它编程语言适用的最佳实践在 Rust 中也可以很好的使用,但是 Rust 并不是一门专门的面向对象或者函数式语言,因此在使用自己喜欢的编程风格时,也要考虑遵循 Rust 应有的实践。
例如纯面向对象或纯函数式编程,在 Rust 中就并不是一个很好的选择。如果你有过Go语言的编程经验相信能更加理解我这里想表达的含义。
例如纯面向对象或纯函数式编程,在 Rust 中就并不是一个很好的选择。如果你有过 Go 语言的编程经验,相信能更加理解我这里想表达的含义。
不过大家也不用担心,在书中我们以专题的形式专门讲解 Rust 的最佳实践,看完后自然就明白了。

Loading…
Cancel
Save