pull/501/head
sunface 3 years ago
commit 5005427c9e

@ -1,4 +1,4 @@
## 附录 D可派生的 trait ## 附录 D派生特征 trait
在本书的各个部分中,我们讨论了可应用于结构体和枚举定义的 `derive` 属性。被 `derive` 标记的对象会自动实现对应的默认特征代码,继承相应的功能。 在本书的各个部分中,我们讨论了可应用于结构体和枚举定义的 `derive` 属性。被 `derive` 标记的对象会自动实现对应的默认特征代码,继承相应的功能。
@ -54,19 +54,19 @@
### 复制值的 `Clone``Copy` ### 复制值的 `Clone``Copy`
`Clone` 特征用于创建一个值的深拷贝deep copy复制过程可能包含代码的执行以及堆上数据的复制。查阅 [“通过Clone来进行深拷贝”](../core/ownership.md#通过Clone来进行深拷贝)获取有关 `Clone` 的更多信息。 `Clone` 特征用于创建一个值的深拷贝deep copy复制过程可能包含代码的执行以及堆上数据的复制。查阅 [通过 Clone 进行深拷贝](https://course.rs/basic/ownership/ownership.html#克隆深拷贝)获取有关 `Clone` 的更多信息。
派生 `Clone` 实现了 `clone` 方法,当为整个的类型实现 `Clone` 时,在该类型的每一部分上都会调用 `clone` 方法。这意味着类型中所有字段或值也必须实现了 `Clone`,这样才能够派生 `Clone` 派生 `Clone` 实现了 `clone` 方法,当为整个的类型实现 `Clone` 时,在该类型的每一部分上都会调用 `clone` 方法。这意味着类型中所有字段或值也必须实现了 `Clone`,这样才能够派生 `Clone`
例如当在一个切片slice上调用 `to_vec` 方法时, `Clone` 是必须的。切片只是一个引用,并不拥有其所包含的实例数据,但是从 `to_vec` 中返回的Vector需要拥有实例数据因此 `to_vec` 需要在每个元素上调用 `clone` 来逐个复制。因此,存储在切片中的类型必须实现 `Clone` 例如当在一个切片slice上调用 `to_vec` 方法时, `Clone` 是必须的。切片只是一个引用,并不拥有其所包含的实例数据,但是从 `to_vec` 中返回的Vector需要拥有实例数据因此 `to_vec` 需要在每个元素上调用 `clone` 来逐个复制。因此,存储在切片中的类型必须实现 `Clone`
`Copy` 特征允许你通过只拷贝存储在栈上的数据来复制值(浅拷贝),而无需复制存储在堆上的底层数据。查阅第四章[通过Copy复制栈数据](../core/ownership.md#通过Copy复制栈数据) 的部分来获取有关 `Copy` 的更多信息。 `Copy` 特征允许你通过只拷贝存储在栈上的数据来复制值(浅拷贝),而无需复制存储在堆上的底层数据。查阅 [通过 Copy 复制栈数据](https://course.rs/basic/ownership/ownership.html#拷贝浅拷贝) 的部分来获取有关 `Copy` 的更多信息。
实际上 `Copy` 特征并不阻止你在实现时使用了深拷贝,只是,我们不应该这么做,毕竟遵循一个语言的惯例是很重要的。当用户看到 `Copy` 时,潜意识就应该知道这是浅拷贝,复制一个值会非常快。 实际上 `Copy` 特征并不阻止你在实现时使用了深拷贝,只是,我们不应该这么做,毕竟遵循一个语言的惯例是很重要的。当用户看到 `Copy` 时,潜意识就应该知道这是浅拷贝,复制一个值会非常快。
当一个类型的内部字段全部实现了 `Copy` 时,你就可以在该类型上派上 `Copy` 特征。 一个类型如果要实现 `Copy` 它必须先实现 `Clone` ,因为一个类型实现 `Clone` 后,就等于顺便实现了 `Copy` 当一个类型的内部字段全部实现了 `Copy` 时,你就可以在该类型上派上 `Copy` 特征。 一个类型如果要实现 `Copy` 它必须先实现 `Clone` ,因为一个类型实现 `Clone` 后,就等于顺便实现了 `Copy`
总之, `Copy` 拥有更好的性能,当浅拷贝足够的时候,就不要使用 `Clone` ,不然会导致你的代码运行更慢,对于[性能优化](../performance/intro.md)来说,一个很大的方面就是减少热点路径深拷贝的发生。 总之, `Copy` 拥有更好的性能,当浅拷贝足够的时候,就不要使用 `Clone` ,不然会导致你的代码运行更慢,对于[性能优化](https://course.rs/profiling/performance/intro.html)来说,一个很大的方面就是减少热点路径深拷贝的发生。
### 固定大小的值映射的 `Hash` ### 固定大小的值映射的 `Hash`

@ -4,7 +4,7 @@
## 字符类型(char) ## 字符类型(char)
字符,对于没有其它编程经验的新手来说可能不太好理解(没有编程经验敢来学 Rust 的绝对是好汉),但是你可以把它理解为英文中的字母,中文中的汉字。 字符,对于没有其它编程经验的新手来说可能不太好理解(没有编程经验敢来学 Rust 的绝对是好汉),但是你可以把它理解为英文中的字母,中文中的汉字。
下面的代码展示了几个颇具异域风情的字符: 下面的代码展示了几个颇具异域风情的字符:
``` ```
@ -39,7 +39,7 @@ $ cargo run
## 布尔(bool) ## 布尔(bool)
Rust 中的布尔类型有两个可能的值:`true` 和 `false`, 布尔值占用内存的大小为 `1` 个字节: Rust 中的布尔类型有两个可能的值:`true` 和 `false`布尔值占用内存的大小为 `1` 个字节:
```rust ```rust
fn main() { fn main() {

@ -12,7 +12,7 @@ Rust 每个值都有其确切的数据类型,总的来说可以分为两类:
## 类型推导与标注 ## 类型推导与标注
`Python` `Javascript` 等动态语言不同Rust 是一门静态类型语言,也就是编译器必须在编译期知道我们所有变量的类型,但这不意味着你需要为每个变量指定类型,因为**Rust 编译器很聪明,它可以根据变量的值和上下文中的使用方式来自动推导出变量的类型**,同时编译器也不够聪明,在某些情况下,它无法推导出变量类型,需要手动去给予一个类型标注,关于这一点在[Rust语言初印象](../../first-try/hello-world.md#Rust语言初印象)中有过展示。 与 Python、Javascript 等动态语言不同Rust 是一门静态类型语言,也就是编译器必须在编译期知道我们所有变量的类型,但这不意味着你需要为每个变量指定类型,因为 **Rust 编译器很聪明,它可以根据变量的值和上下文中的使用方式来自动推导出变量的类型**,同时编译器也不够聪明,在某些情况下,它无法推导出变量类型,需要手动去给予一个类型标注,关于这一点在 [Rust 语言初印象](https://course.rs/first-try/hello-world.html#rust-语言初印象)中有过展示。
来看段代码: 来看段代码:
```rust ```rust
@ -30,5 +30,5 @@ error[E0282]: type annotations needed
| ^^^^^ consider giving `guess` a type | ^^^^^ consider giving `guess` a type
``` ```
因此我们需要提供给编译器更多的信息,例如给 `guess` 变量一个**显式的类型标注** `let guess: i32 = ...` 或者 `"42".parse::<i32>()` 因此我们需要提供给编译器更多的信息,例如给 `guess` 变量一个**显式的类型标注**`let guess: i32 = ...` 或者 `"42".parse::<i32>()`

@ -7,7 +7,7 @@
Rust 使用一个相对传统的语法来创建整数(`1``2`...)和浮点数(`1.0``1.1`...)。整数、浮点数的运算和你在其它语言上见过的一致,都是通过常见的运算符来完成。 Rust 使用一个相对传统的语法来创建整数(`1``2`...)和浮点数(`1.0``1.1`...)。整数、浮点数的运算和你在其它语言上见过的一致,都是通过常见的运算符来完成。
> 不仅仅是数值类型Rust 也允许在复杂类型上定义运算符,例如在自定义类型上定义 `+` 运算符这种行为被称为运算符重载Rust 具体支持的可重载运算符见[这里](../../appendix/operators.md#运算符) > 不仅仅是数值类型Rust 也允许在复杂类型上定义运算符,例如在自定义类型上定义 `+` 运算符这种行为被称为运算符重载Rust 具体支持的可重载运算符见[附录 B](https://course.rs/appendix/operators.html#运算符)
#### 整数类型 #### 整数类型
@ -97,7 +97,7 @@ fn main() {
} }
``` ```
这些语句中的每个表达式都使用了数学运算符,并且计算结果为一个值,然后绑定到一个变量上。[附录](../../appendix/operators.md)中给出了 Rust 提供的所有运算符的列表。 这些语句中的每个表达式都使用了数学运算符,并且计算结果为一个值,然后绑定到一个变量上。[附录 B](https://course.rs/appendix/operators.html#运算符) 中给出了 Rust 提供的所有运算符的列表。
再来看一个综合性的示例: 再来看一个综合性的示例:
@ -227,7 +227,7 @@ fn main() {
## 序列(Range) ## 序列(Range)
Rust 提供了一个非常简洁的方式,用来生成连续的数值,例如 `1..5` ,生成从 1 到 4 的连续数字,不包含 5 `1..=5` ,生成从 1 到 5 的连续数字,包含 5 ,它的用途很简单,常常用于循环中: Rust 提供了一个非常简洁的方式,用来生成连续的数值,例如 `1..5`,生成从 1 到 4 的连续数字,不包含 5 `1..=5`,生成从 1 到 5 的连续数字,包含 5它的用途很简单常常用于循环中
```rust ```rust
for i in 1..=5 { for i in 1..=5 {
@ -286,7 +286,7 @@ use num::complex::Complex;
- **Rust 拥有相当多的数值类型**. 因此你需要熟悉这些类型所占用的字节数,这样就知道该类型允许的大小范围以及你选择的类型是否能表达负数 - **Rust 拥有相当多的数值类型**. 因此你需要熟悉这些类型所占用的字节数,这样就知道该类型允许的大小范围以及你选择的类型是否能表达负数
- **类型转换必须是显式的**. Rust 永远也不会偷偷把你的 16bit 整数转换成 32bit 整数 - **类型转换必须是显式的**. Rust 永远也不会偷偷把你的 16bit 整数转换成 32bit 整数
- **Rust 的数值上可以使用方法**. 例如你可以用以下方法来将 `13.14` 取整: `13.14_f32.round()`,在这里我们使用了类型后缀,因为编译器需要知道 `13.14 `的具体类型 - **Rust 的数值上可以使用方法**. 例如你可以用以下方法来将 `13.14` 取整:`13.14_f32.round()`,在这里我们使用了类型后缀,因为编译器需要知道 `13.14` 的具体类型
数值类型的讲解已经基本结束,接下来,来看看字符和布尔类型。 数值类型的讲解已经基本结束,接下来,来看看字符和布尔类型。

@ -29,7 +29,7 @@ fn main() {
} }
``` ```
接下来我们的学习非常类似原型设计:有的方法只提供 API 接口但是不提供具体实现。此外有的变量在声明之后并未使用因此在这个阶段我们需要排除一些编译器噪音Rust 在编译的时候会扫描代码,变量声明后未使用会以 `warning` 警告的形式进行提示),引入 `#![allow(unused_variables)]` 属性标记,该标记会告诉编译器忽略未使用的变量,不要抛出 `warning` 警告,具体的常见编译器属性你可以在这里查阅:[编译器属性标记](../../profiling/compiler/attributes.md). 接下来我们的学习非常类似原型设计:有的方法只提供 API 接口但是不提供具体实现。此外有的变量在声明之后并未使用因此在这个阶段我们需要排除一些编译器噪音Rust 在编译的时候会扫描代码,变量声明后未使用会以 `warning` 警告的形式进行提示),引入 `#![allow(unused_variables)]` 属性标记,该标记会告诉编译器忽略未使用的变量,不要抛出 `warning` 警告,具体的常见编译器属性你可以在这里查阅:[编译器属性标记](https://course.rs/profiling/compiler/attributes.html)。
`read` 函数也非常有趣,它返回一个 `!` 类型,这个表明该函数是一个发散函数,不会返回任何值,包括 `()`。`unimplemented!()` 告诉编译器该函数尚未实现,`unimplemented!()` 标记通常意味着我们期望快速完成主要代码,回头再通过搜索这些标记来完成次要代码,类似的标记还有 `todo!()`,当代码执行到这种未实现的地方时,程序会直接报错。你可以反注释 `read(&mut f1, &mut vec![]);` 这行,然后再观察下结果。 `read` 函数也非常有趣,它返回一个 `!` 类型,这个表明该函数是一个发散函数,不会返回任何值,包括 `()`。`unimplemented!()` 告诉编译器该函数尚未实现,`unimplemented!()` 标记通常意味着我们期望快速完成主要代码,回头再通过搜索这些标记来完成次要代码,类似的标记还有 `todo!()`,当代码执行到这种未实现的地方时,程序会直接报错。你可以反注释 `read(&mut f1, &mut vec![]);` 这行,然后再观察下结果。

@ -1,6 +1,6 @@
# 字符串 # 字符串
在其他语言,字符串往往是送分题,因为实在是太简单了,例如 `"hello, world"` 就是字符串章节的几乎全部内容了,但是如果你带着同样的想法来学 Rust我保证绝对会栽跟头**因此这一章大家一定要重视,仔细阅读,这里有很多其它 Rust 书籍中没有的内容**。 在其他语言,字符串往往是送分题,因为实在是太简单了,例如 `"hello, world"` 就是字符串章节的几乎全部内容了,但是如果你带着同样的想法来学 Rust我保证绝对会栽跟头 **因此这一章大家一定要重视,仔细阅读,这里有很多其它 Rust 书籍中没有的内容**。
首先来看段很简单的代码: 首先来看段很简单的代码:
```rust ```rust
@ -53,7 +53,7 @@ let world = &s[6..11];
<img alt="" src="https://pic1.zhimg.com/80/v2-69da917741b2c610732d8526a9cc86f5_1440w.jpg" class="center" style="width: 50%;" /> <img alt="" src="https://pic1.zhimg.com/80/v2-69da917741b2c610732d8526a9cc86f5_1440w.jpg" class="center" style="width: 50%;" />
在使用 Rust 的 `..` [range序列](../base-type/numbers.md#序列(Range))语法时,如果你想从索引 0 开始,可以使用如下的方式,这两个是等效的: 在使用 Rust 的 `..` [range序列](https://course.rs/base-type/numbers.html#序列range)语法时,如果你想从索引 0 开始,可以使用如下的方式,这两个是等效的:
```rust ```rust
let s = String::from("hello"); let s = String::from("hello");
@ -81,14 +81,14 @@ let slice = &s[0..len];
let slice = &s[..]; let slice = &s[..];
``` ```
>在对字符串使用切片语法时需要格外小心切片的索引必须落在字符之间的边界位置也就是UTF8字符的边界例如中文在UTF8中占用三个字节,下面的代码就会崩溃: >在对字符串使用切片语法时需要格外小心切片的索引必须落在字符之间的边界位置也就是UTF-8字符的边界例如中文在UTF-8中占用三个字节,下面的代码就会崩溃:
>```rust >```rust
> let s = "中国人"; > let s = "中国人";
> let a = &s[0..2]; > let a = &s[0..2];
> println!("{}",a); > println!("{}",a);
>``` >```
>因为我们只取 `s` 字符串的前两个字节,但是本例中每个汉字占用三个字节,因此没有落在边界处,也就是连 `中` 字都取不完整,此时程序会直接崩溃退出,如果改成 `&s[0..3]`,则可以正常通过编译。 >因为我们只取 `s` 字符串的前两个字节,但是本例中每个汉字占用三个字节,因此没有落在边界处,也就是连 `中` 字都取不完整,此时程序会直接崩溃退出,如果改成 `&s[0..3]`,则可以正常通过编译。
> 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点, 关于该如何操作 UTF8 字符串,参见[这里](#操作UTF8字符串) > 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点, 关于该如何操作 UTF-8 字符串,参见[这里](#操作-UTF8-字符串)
字符串切片的类型标识是 `&str`,因此我们可以这样声明一个函数,输入 `String` 类型,返回它的切片: `fn first_word(s: &String) -> &str ` 字符串切片的类型标识是 `&str`,因此我们可以这样声明一个函数,输入 `String` 类型,返回它的切片: `fn first_word(s: &String) -> &str `
@ -155,11 +155,11 @@ let s: &str = "Hello, world!";
## 什么是字符串? ## 什么是字符串?
顾名思义,字符串是由字符组成的连续集合,但是在上一节中我们提到过,**Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4)**,这样有助于大幅降低字符串所占用的内存空间。 顾名思义,字符串是由字符组成的连续集合,但是在上一节中我们提到过,**Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4)**,这样有助于大幅降低字符串所占用的内存空间。
Rust 在语言级别,只有一种字符串类型: `str`,它通常是以引用类型出现 `&str`,也就是上文提到的字符串切片。虽然语言级别只有上述的 `str` 类型,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是 `String` 类型。 Rust 在语言级别,只有一种字符串类型: `str`,它通常是以引用类型出现 `&str`,也就是上文提到的字符串切片。虽然语言级别只有上述的 `str` 类型,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是 `String` 类型。
`str` 类型是硬编码进可执行文件,也无法被修改,但是 `String` 则是一个可增长、可改变且具有所有权的 UTF8 编码字符串,**当 Rust 用户提到字符串时,往往指的就是 `String` 类型和 `&str` 字符串切片类型,这两个类型都是 UTF8 编码**。 `str` 类型是硬编码进可执行文件,也无法被修改,但是 `String` 则是一个可增长、可改变且具有所有权的 UTF-8 编码字符串,**当 Rust 用户提到字符串时,往往指的就是 `String` 类型和 `&str` 字符串切片类型,这两个类型都是 UTF-8 编码**。
除了 `String` 类型的字符串Rust 的标准库还提供了其他类型的字符串,例如 `OsString` `OsStr` `CsString` 和` CsStr` 等,注意到这些名字都以 `String` 或者 `Str` 结尾了吗?它们分别对应的是具有所有权和被借用的变量。 除了 `String` 类型的字符串Rust 的标准库还提供了其他类型的字符串,例如 `OsString` `OsStr` `CsString` 和` CsStr` 等,注意到这些名字都以 `String` 或者 `Str` 结尾了吗?它们分别对应的是具有所有权和被借用的变量。
@ -185,7 +185,7 @@ fn main() {
assert_eq!(s,"hello,world!"); assert_eq!(s,"hello,world!");
// 从现有的&str切片创建String类型 // 从现有的&str切片创建String类型
// String与&str都是UTF8编码因此支持中文 // String与&str都是UTF-8编码因此支持中文
let mut s = String::from("你好,世界"); let mut s = String::from("你好,世界");
// 将字符'!'推入s中 // 将字符'!'推入s中
s.push('!'); s.push('!');
@ -243,7 +243,7 @@ fn say_hello(s: &str) {
} }
``` ```
实际上这种灵活用法是因为 `deref` 隐式强制转换,具体我们会在 [Deref特征](../../traits/deref.md)进行详细讲解。 实际上这种灵活用法是因为 `deref` 隐式强制转换,具体我们会在 [`Deref` 特征](https://course.rs/advance/smart-pointer/deref.html)进行详细讲解。
## 字符串索引 ## 字符串索引
@ -262,11 +262,11 @@ 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"` 中的每个字母在 UTF-8 编码中仅占用 1 个字节,但是对于下面的代码呢?
```rust ```rust
let hello = String::from("中国人"); let hello = String::from("中国人");
``` ```
如果问你该字符串多长,你可能会说 `3`,但是实际上是 `9` 个字节的长度,因为大部分常用汉字在 UTF8 中的长度是 `3` 个字节,因此这种情况下对 `hello` 进行索引,访问 `&hello[0]` 没有任何意义,因为你取不到 `中` 这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的返回值。 如果问你该字符串多长,你可能会说 `3`,但是实际上是 `9` 个字节的长度,因为大部分常用汉字在 UTF-8 中的长度是 `3` 个字节,因此这种情况下对 `hello` 进行索引,访问 `&hello[0]` 没有任何意义,因为你取不到 `中` 这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的返回值。
#### 字符串的不同表现形式 #### 字符串的不同表现形式
现在看一下用梵文写的字符串 `“नमस्ते”`, 它底层的字节数组如下形式: 现在看一下用梵文写的字符串 `“नमस्ते”`, 它底层的字节数组如下形式:
@ -288,7 +288,7 @@ let hello = String::from("中国人");
还有一个原因导致了 Rust 不允许去索引字符串:因为索引操作,我们总是期望它的性能表现是 O(1),然而对于 `String` 类型来说,无法保证这一点,因为 Rust 可能需要从 0 开始去遍历字符串来定位合法的字符。 还有一个原因导致了 Rust 不允许去索引字符串:因为索引操作,我们总是期望它的性能表现是 O(1),然而对于 `String` 类型来说,无法保证这一点,因为 Rust 可能需要从 0 开始去遍历字符串来定位合法的字符。
## 字符串切片 ## 字符串切片
前文提到过,字符串切片是非常危险的操作,因为切片的索引是通过字节来进行,但是字符串又是 UTF8 编码,因此你无法保证索引的字节刚好落在字符的边界上,例如: 前文提到过,字符串切片是非常危险的操作,因为切片的索引是通过字节来进行,但是字符串又是 UTF-8 编码,因此你无法保证索引的字节刚好落在字符的边界上,例如:
```rust ```rust
let hello = "中国人"; let hello = "中国人";
@ -303,8 +303,8 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
因此在通过索引区间来访问字符串时,**需要格外的小心**,一不注意,就会导致你程序的崩溃! 因此在通过索引区间来访问字符串时,**需要格外的小心**,一不注意,就会导致你程序的崩溃!
## 操作UTF8字符串 ## 操作 UTF8 字符串
前文提到了几种使用 UTF8 字符串的方式,下面来一一说明。 前文提到了几种使用 UTF-8 字符串的方式,下面来一一说明。
#### 字符 #### 字符
如果你想要以 Unicode 字符的方式遍历字符串,最好的办法是使用 `chars` 方法,例如: 如果你想要以 Unicode 字符的方式遍历字符串,最好的办法是使用 `chars` 方法,例如:
@ -341,7 +341,7 @@ for b in "中国人".bytes() {
``` ```
#### 获取子串 #### 获取子串
想要准确的从UTF8字符串中获取子串是较为复杂的事情例如想要从 `holla中国人नमस्ते` 这种变长的字符串中取出某一个子串,使用标准库你是做不到的。 想要准确的从UTF-8字符串中获取子串是较为复杂的事情例如想要从 `holla中国人नमस्ते` 这种变长的字符串中取出某一个子串,使用标准库你是做不到的。
你需要在 `crates.io` 上搜索 `utf8` 来寻找想要的功能。 你需要在 `crates.io` 上搜索 `utf8` 来寻找想要的功能。
可以考虑尝试下这个库:[utf8_slice](https://crates.io/crates/utf8_slice)。 可以考虑尝试下这个库:[utf8_slice](https://crates.io/crates/utf8_slice)。
@ -355,6 +355,7 @@ for b in "中国人".bytes() {
就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串字面值的不可变性。不幸的是,我们不能为了获得这种性能,而把每一个在编译时大小未知的文本都放进内存中(你也做不到!),因为有的字符串是在程序运行得过程中动态生成的。 就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串字面值的不可变性。不幸的是,我们不能为了获得这种性能,而把每一个在编译时大小未知的文本都放进内存中(你也做不到!),因为有的字符串是在程序运行得过程中动态生成的。
对于 `String` 类型,为了支持一个可变、可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容,这些都是在程序运行时完成的: 对于 `String` 类型,为了支持一个可变、可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容,这些都是在程序运行时完成的:
- 首先向操作系统请求内存来存放 `String` 对象 - 首先向操作系统请求内存来存放 `String` 对象
- 在使用完成后,将内存释放,归还给操作系统 - 在使用完成后,将内存释放,归还给操作系统

@ -9,6 +9,7 @@
#### 定义结构体 #### 定义结构体
一个结构体有几部分组成: 一个结构体有几部分组成:
- 通过关键字 `struct` 定义 - 通过关键字 `struct` 定义
- 一个清晰明确的结构体 `名称` - 一个清晰明确的结构体 `名称`
- 几个有名字的结构体 `字段` - 几个有名字的结构体 `字段`
@ -105,7 +106,7 @@ fn build_user(email: String, username: String) -> User {
> >
> 聪明的读者肯定要发问了:明明有三个字段进行了自动赋值,为何只有 `username` 发生了所有权转移? > 聪明的读者肯定要发问了:明明有三个字段进行了自动赋值,为何只有 `username` 发生了所有权转移?
> >
> 仔细回想一下[所有权](../ownership/ownership.md#拷贝(浅拷贝))那一节的内容,我们提到了 `Copy` 特征:实现了 `Copy` 特征的类型无需所有权转移,可以直接在赋值时进行 > 仔细回想一下[所有权](../ownership/ownership.md#拷贝浅拷贝)那一节的内容,我们提到了 `Copy` 特征:实现了 `Copy` 特征的类型无需所有权转移,可以直接在赋值时进行
> 数据拷贝,其中 `bool``u64` 类型就实现了 `Copy` 特征,因此 `active``sign_in_count` 字段在赋值给 `user2` 时,仅仅发生了拷贝,而不是所有权转移。 > 数据拷贝,其中 `bool``u64` 类型就实现了 `Copy` 特征,因此 `active``sign_in_count` 字段在赋值给 `user2` 时,仅仅发生了拷贝,而不是所有权转移。
> >
> 值得注意的是:`username` 所有权被转移给了 `user2`,导致了 `user1` 无法再被使用,但是并不代表 `user1` 内部的其它字段不能被继续使用,例如: > 值得注意的是:`username` 所有权被转移给了 `user2`,导致了 `user1` 无法再被使用,但是并不代表 `user1` 内部的其它字段不能被继续使用,例如:
@ -201,7 +202,7 @@ impl SomeTrait for AlwaysEqual {
在之前的 `User` 结构体的定义中,有一处细节:我们使用了自身拥有所有权的 `String` 类型而不是基于引用的 `&str` 字符串切片类型。这是一个有意而为之的选择:因为我们想要这个结构体拥有它所有的数据,而不是从其它地方借用数据。 在之前的 `User` 结构体的定义中,有一处细节:我们使用了自身拥有所有权的 `String` 类型而不是基于引用的 `&str` 字符串切片类型。这是一个有意而为之的选择:因为我们想要这个结构体拥有它所有的数据,而不是从其它地方借用数据。
你也可以让 `User` 结构体从其它对象借用数据,不过这么做,就需要引入[生命周期lifetimes](../../advance/lifetime/basic.md)这个新概念(也是一个复杂的概念),简而言之,生命周期能确保结构体的作用范围要比它所借用的数据的作用范围要小。 你也可以让 `User` 结构体从其它对象借用数据,不过这么做,就需要引入[生命周期(lifetimes)](../../advance/lifetime/basic.md)这个新概念(也是一个复杂的概念),简而言之,生命周期能确保结构体的作用范围要比它所借用的数据的作用范围要小。
总之,如果你想在结构体中使用一个引用,就必须加上生命周期,否则就会报错: 总之,如果你想在结构体中使用一个引用,就必须加上生命周期,否则就会报错:
@ -273,7 +274,7 @@ fn main() {
} }
``` ```
首先可以观察到,上面使用了 `{}` 而不是之前的 `{:}`,运行后报错: 首先可以观察到,上面使用了 `{}` 而不是之前的 `{:?}`,运行后报错:
```shell ```shell
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display` error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
``` ```
@ -284,7 +285,7 @@ fn main() {
let v = 1; let v = 1;
let b = true; let b = true;
println!("{}, {}",v,b); println!("{}, {}", v, b);
} }
``` ```
@ -319,7 +320,7 @@ error[E0277]: `Rectangle` doesn't implement `Debug`
- 手动实现 - 手动实现
- 使用 `derive` 派生实现 - 使用 `derive` 派生实现
后者简单的多,但是也有限制,具体见[附录](https://course.rs/appendix/derive.html),这里我们就不再深入讲解,来看看该如何使用: 后者简单的多,但是也有限制,具体见[附录 D](https://course.rs/appendix/derive.html),这里我们就不再深入讲解,来看看该如何使用:
```rust ```rust
#[derive(Debug)] #[derive(Debug)]
struct Rectangle { struct Rectangle {
@ -355,9 +356,9 @@ rect1 is Rectangle {
此时结构体的输出跟我们创建时候的代码几乎一模一样了!当然,如果大家还是不满足,那最好还是自己实现 `Display` 特征,以向用户更美的展示你的私藏结构体。关于格式化输出的更多内容,我们强烈推荐看看这个[章节](https://course.rs/basic/formatted-output.html#debug-特征)。 此时结构体的输出跟我们创建时候的代码几乎一模一样了!当然,如果大家还是不满足,那最好还是自己实现 `Display` 特征,以向用户更美的展示你的私藏结构体。关于格式化输出的更多内容,我们强烈推荐看看这个[章节](https://course.rs/basic/formatted-output.html#debug-特征)。
还有一个简单的输出 debug 信息的方法,那就是使用 [`dbg!` 宏](https://doc.rust-lang.org/std/macro.dbg.html),它会拿走表达式的所有权,然后打出相应的文件名、行号等 debug 信息,当然还有我们需要的表达式的求值结果。**除之外,它最终还会把表达式值的所有权返回!** 还有一个简单的输出 debug 信息的方法,那就是使用 [`dbg!` 宏](https://doc.rust-lang.org/std/macro.dbg.html),它会拿走表达式的所有权,然后打出相应的文件名、行号等 debug 信息,当然还有我们需要的表达式的求值结果。**除之外,它最终还会把表达式值的所有权返回!**
> `dbg!` 输出到的是标准错误输出 `stderr`,而 `println!` 输出到标准输出 `stdout` > `dbg!` 输出到标准错误输出 `stderr`,而 `println!` 输出到标准输出 `stdout`
下面的例子中清晰的展示了 `dbg!` 如何在打印出信息的同时,还把表达式的值赋给了 `width`: 下面的例子中清晰的展示了 `dbg!` 如何在打印出信息的同时,还把表达式的值赋给了 `width`:
```rust ```rust

@ -27,7 +27,7 @@ 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 ```rust
@ -66,4 +66,4 @@ fn calculate_length(s: String) -> (String, usize) {
在其他语言中,可以用结构体来声明一个三维空间中的点,例如 `Point(10, 20, 30)`,虽然使用 Rust 元组也可以做到:`(10, 20, 30)`,但是这样写有个非常重大的缺陷: 在其他语言中,可以用结构体来声明一个三维空间中的点,例如 `Point(10, 20, 30)`,虽然使用 Rust 元组也可以做到:`(10, 20, 30)`,但是这样写有个非常重大的缺陷:
**不具备任何清晰的含义**,在下一章节中,会提到一种与元组类似的结构体, `元组结构体`,可以解决这个问题。 **不具备任何清晰的含义**,在下一章节中,会提到一种与元组类似的结构体,`元组结构体`,可以解决这个问题。

@ -2,7 +2,7 @@
上节中提到,如果仅仅支持通过转移所有权的方式获取一个值,那会让程序变得复杂。 Rust 能否像其它编程语言一样,使用某个变量的指针或者引用呢?答案是可以。 上节中提到,如果仅仅支持通过转移所有权的方式获取一个值,那会让程序变得复杂。 Rust 能否像其它编程语言一样,使用某个变量的指针或者引用呢?答案是可以。
Rust 通过 `借用(Borrowing)` 这个概念来达成上述的目的: **获取变量的引用,称之为借用(borrowing)**。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来,当使用完毕后,也必须要物归原主。 Rust 通过 `借用(Borrowing)` 这个概念来达成上述的目的 **获取变量的引用,称之为借用(borrowing)**。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来,当使用完毕后,也必须要物归原主。
@ -70,7 +70,7 @@ fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
// 所以什么也不会发生 // 所以什么也不会发生
``` ```
人总是贪心的,可以拉女孩小手了,就想着抱抱柔软的身子(读者中的某老司机表示,这个流程完全不对),因此光借用已经满足不了我们了,如果尝试修改借用的变量呢? 人总是贪心的,可以拉女孩小手了,就想着抱抱柔软的身子(读者中的某老司机表示,这个流程完全不对),因此光借用已经满足不了我们了,如果尝试修改借用的变量呢?
```rust ```rust
fn main() { fn main() {
let s = String::from("hello"); let s = String::from("hello");
@ -146,6 +146,7 @@ error[E0499]: cannot borrow `s` as mutable more than once at a time 同一时间
对于新手来说,这个特性绝对是一大拦路虎,也是新人们谈之色变的编译器 `borrow checker` 特性之一不过各行各业都一样限制往往是出于安全的考虑Rust 也一样。 对于新手来说,这个特性绝对是一大拦路虎,也是新人们谈之色变的编译器 `borrow checker` 特性之一不过各行各业都一样限制往往是出于安全的考虑Rust 也一样。
这种限制的好处就是使 Rust 在编译期就避免数据竞争,数据竞争可由以下行为造成: 这种限制的好处就是使 Rust 在编译期就避免数据竞争,数据竞争可由以下行为造成:
- 两个或更多的指针同时访问同一数据 - 两个或更多的指针同时访问同一数据
- 至少有一个指针被用来写入数据 - 至少有一个指针被用来写入数据
- 没有同步数据访问的机制 - 没有同步数据访问的机制
@ -213,7 +214,7 @@ fn main() {
// 新编译器中r3作用域在这里结束 // 新编译器中r3作用域在这里结束
``` ```
在老版本的编译器中Rust 1.31前),将会报错,因为 `r1``r2` 的作用域在花括号 `}` 处结束,那么 `r3` 的借用就会触发 **无法同时借用可变和不可变**的规则。 在老版本的编译器中Rust 1.31 前),将会报错,因为 `r1``r2` 的作用域在花括号 `}` 处结束,那么 `r3` 的借用就会触发 **无法同时借用可变和不可变**的规则。
但是在新的编译器中,该代码将顺利通过,因为 **引用作用域的结束位置从花括号变成最后一次使用的位置**,因此 `r1` 借用和 `r2` 借用在 `println!` 后,就结束了,此时 `r3` 可以顺利借用到可变引用。 但是在新的编译器中,该代码将顺利通过,因为 **引用作用域的结束位置从花括号变成最后一次使用的位置**,因此 `r1` 借用和 `r2` 借用在 `println!` 后,就结束了,此时 `r3` 可以顺利借用到可变引用。
@ -224,7 +225,7 @@ fn main() {
虽然这种借用错误有的时候会让我们很郁闷,但是你只要想想这是 Rust 提前帮你发现了潜在的 BUG其实就开心了虽然减慢了开发速度但是从长期来看大幅减少了后续开发和运维成本。 虽然这种借用错误有的时候会让我们很郁闷,但是你只要想想这是 Rust 提前帮你发现了潜在的 BUG其实就开心了虽然减慢了开发速度但是从长期来看大幅减少了后续开发和运维成本。
### 悬垂引用Dangling References ### 悬垂引用(Dangling References)
悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。在 Rust 中编译器可以确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器可以确保数据不会在其引用之前被释放,要想释放数据,必须先停止其引用的使用。 悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。在 Rust 中编译器可以确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器可以确保数据不会在其引用之前被释放,要想释放数据,必须先停止其引用的使用。
@ -259,7 +260,7 @@ help: consider using the `'static` lifetime
``` ```
错误信息引用了一个我们还未介绍的功能:[生命周期lifetimes](../../advance/lifetime/basic.md)。不过,即使你不理解生命周期,也可以通过错误信息知道这段代码错误的关键信息: 错误信息引用了一个我们还未介绍的功能:[生命周期(lifetimes)](https://course.rs/advance/lifetime/basic.html)。不过,即使你不理解生命周期,也可以通过错误信息知道这段代码错误的关键信息:
```text ```text
this function's return type contains a borrowed value, but there is no value for it to be borrowed from. this function's return type contains a borrowed value, but there is no value for it to be borrowed from.
@ -297,5 +298,6 @@ fn no_dangle() -> String {
## 借用规则总结 ## 借用规则总结
总的来说,借用规则如下: 总的来说,借用规则如下:
- 同一时刻,你只能拥有要么一个可变引用, 要么任意多个不可变引用 - 同一时刻,你只能拥有要么一个可变引用, 要么任意多个不可变引用
- 引用必须总是有效的 - 引用必须总是有效的

@ -30,7 +30,7 @@ int* foo() {
在正式进入主题前,先来一个预热知识。 在正式进入主题前,先来一个预热知识。
## 栈Stack与堆Heap ## 栈(Stack)与堆(Heap)
栈和堆是编程语言最核心的数据结构,但是在很多语言中,你并不需要深入了解栈与堆。 但对于 Rust 这样的系统编程语言,值是位于栈上还是堆上非常重要, 因为这会影响程序的行为和性能。 栈和堆是编程语言最核心的数据结构,但是在很多语言中,你并不需要深入了解栈与堆。 但对于 Rust 这样的系统编程语言,值是位于栈上还是堆上非常重要, 因为这会影响程序的行为和性能。
@ -49,11 +49,11 @@ int* foo() {
与栈不同,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。 与栈不同,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。
当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的**指针**, 该过程被称为**在堆上分配内存**,有时简称为 “分配”allocating 当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的**指针**, 该过程被称为**在堆上分配内存**,有时简称为 “分配”(allocating)
接着,该指针会被推入**栈**中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的**指针**,来获取数据在堆上的实际内存位置,进而访问该数据。 接着,该指针会被推入**栈**中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的**指针**,来获取数据在堆上的实际内存位置,进而访问该数据。
由上可知,堆是一种缺乏组织的数据结构。想象一下去餐馆就座吃饭: 进入餐馆,告知服务员有几个人,然后服务员找到一个够大的空桌子(堆上分配的内存空间)并领你们过去。如果有人来迟了,他们也可以通过桌号(栈上的指针)来找到你们坐在哪。 由上可知,堆是一种缺乏组织的数据结构。想象一下去餐馆就座吃饭: 进入餐馆,告知服务员有几个人,然后服务员找到一个够大的空桌子(堆上分配的内存空间)并领你们过去。如果有人来迟了,他们也可以通过桌号(栈上的指针)来找到你们坐在哪。
#### 性能区别 #### 性能区别
@ -77,7 +77,7 @@ int* foo() {
理解了堆栈,接下来看一下*关于所有权的规则*,首先请谨记以下规则: 理解了堆栈,接下来看一下*关于所有权的规则*,首先请谨记以下规则:
> 1. Rust 中每一个值都 `有且只有` 一个所有者(变量) > 1. Rust 中每一个值都 `有且只有` 一个所有者(变量)
> 2. 当所有者(变量)离开作用域范围时,这个值将被丢弃(free) > 2. 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
@ -189,9 +189,9 @@ error[E0382]: use of moved value: `s1`
现在再回头看看之前的规则,相信大家已经有了更深刻的理解: 现在再回头看看之前的规则,相信大家已经有了更深刻的理解:
> 1. Rust 中每一个值都 `有且只有` 一个所有者(变量) > 1. Rust 中每一个值都 `有且只有` 一个所有者(变量)
> 2. 当所有者(变量)离开作用域范围时,这个值将被丢弃(free) > 2. 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
如果你在其他语言中听说过术语**浅拷贝( shallow copy )**和**深拷贝( deep copy )**,那么拷贝指针、长度和容量而不拷贝数据听起来就像浅拷贝,但是又因为 Rust 同时使第一个变量 `s1` 无效了,因此这个操作被称为**移动move**,而不是浅拷贝。上面的例子可以解读为 `s1` 被**移动**到了 `s2` 中。那么具体发生了什么,用一张图简单说明: 如果你在其他语言中听说过术语 **浅拷贝(shallow copy)** **深拷贝(deep copy)**,那么拷贝指针、长度和容量而不拷贝数据听起来就像浅拷贝,但是又因为 Rust 同时使第一个变量 `s1` 无效了,因此这个操作被称为 **移动(move)**,而不是浅拷贝。上面的例子可以解读为 `s1` 被**移动**到了 `s2` 中。那么具体发生了什么,用一张图简单说明:
<img alt="s1 moved to s2" src="https://pic1.zhimg.com/80/v2-3ec77951de6a17584b5eb4a3838b4b61_1440w.jpg" class="center" style="width: 50%;" /> <img alt="s1 moved to s2" src="https://pic1.zhimg.com/80/v2-3ec77951de6a17584b5eb4a3838b4b61_1440w.jpg" class="center" style="width: 50%;" />
@ -248,7 +248,7 @@ println!("x = {}, y = {}", x, y);
Rust 有一个叫做 `Copy` 的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 `Copy` 特征,一个旧的变量在被赋值给其他变量后仍然可用。 Rust 有一个叫做 `Copy` 的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 `Copy` 特征,一个旧的变量在被赋值给其他变量后仍然可用。
那么什么类型是可 `Copy` 的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则:**任何基本类型的组合可以 `Copy` ,不需要分配内存或某种形式资源的类型是可以 `Copy` 的**。如下是一些 `Copy` 的类型: 那么什么类型是可 `Copy` 的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则: **任何基本类型的组合可以 `Copy` ,不需要分配内存或某种形式资源的类型是可以 `Copy` 的**。如下是一些 `Copy` 的类型:
* 所有整数类型,比如 `u32` * 所有整数类型,比如 `u32`
* 布尔类型,`bool`,它的值是 `true``false` * 布尔类型,`bool`,它的值是 `true``false`
@ -316,5 +316,5 @@ fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用
``` ```
所有权很强大,避免了内存的不安全性,但是也带来了一个新麻烦:**总是把一个值传来传去来使用它**。 传入一个函数很可能还要从该函数传出去结果就是语言表达变得非常啰嗦幸运的是Rust 提供了新功能解决这个问题。 所有权很强大,避免了内存的不安全性,但是也带来了一个新麻烦 **总是把一个值传来传去来使用它**。 传入一个函数很可能还要从该函数传出去结果就是语言表达变得非常啰嗦幸运的是Rust 提供了新功能解决这个问题。

Loading…
Cancel
Save