修复一些文字错误

pull/99/head
sunface 3 years ago
parent 67ae82f0ac
commit 8192bc1358

@ -1,40 +1,39 @@
# 复合类型
行百里者半50,欢迎大家来到这里,虽然还不到中点,但是已经不远了。如果说之前学的基础数据类型是原子,那么本章将讲的数据类型可以认为是分子。
行百里者半五十,欢迎大家来到这里,虽然还不到中点,但是已经不远了。如果说之前学的基础数据类型是原子,那么本章将讲的数据类型可以认为是分子。
本章的重点在复合类型上,顾名思义,复合类型是由其它类型组合而来,最典型的就是结构体`struct`和枚举`enum`。例如一个2D的点`point(x,y)`似乎从两个数值类型组合而来。我们不想单独去维护这两个数值,而是希望把它们看作一个整体去认识和处理。
本章的重点在复合类型上,顾名思义,复合类型是由其它类型组合而来,最典型的就是结构体`struct`和枚举`enum`。例如一个2D的点`point(x,y)`,它从两个数值类型组合而来。我们不想单独去维护这两个数值,而是希望把它们看作一个整体去认识和处理。
来看一段代码,它使用我们之前学过的内容来构建文件操作:
```rust
#![allow(unused_variables)]
type File = String;
fn open(f: &mut File) -> bool {
true
}
fn close(f: &mut File) -> bool {
true
}
#[allow(dead_code)]
fn read(f: &mut File, save_to: &mut Vec<u8>) -> ! {
unimplemented!()
}
fn main() {
let mut f1 = File::from("f1.txt");
open(&mut f1);
//read(&mut f1, &mut vec![]);
close(&mut f1);
}
type File = String;
fn open(f: &mut File) -> bool {
true
}
fn close(f: &mut File) -> bool {
true
}
#[allow(dead_code)]
fn read(f: &mut File, save_to: &mut Vec<u8>) -> ! {
unimplemented!()
}
fn main() {
let mut f1 = File::from("f1.txt");
open(&mut f1);
//read(&mut f1, &mut vec![]);
close(&mut f1);
}
```
前阶段非常类似原型设计提供api接口但是不去实现它们。因此在这个阶段我们需要排除一些编译器噪音引入了`#![allow(unused_variables)]`属性标记,该标记会告诉编译器无视未使用的变量,不要抛出`warning`警告,具体的常见编译器属性你可以在这里查阅:[编译器属性标记](../../compiler/attributes.md).
前阶段非常类似原型设计提供api接口但是不去实现它们。因此在这个阶段我们需要排除一些编译器噪音引入了`#![allow(unused_variables)]`属性标记,该标记会告诉编译器无视未使用的变量,不要抛出`warning`警告,具体的常见编译器属性你可以在这里查阅:[编译器属性标记](../../compiler/attributes.md).
`read`函数也非常有趣,它返回一个`!`,这个表明该函数是一个发散函数,不会返回任何值,包括`()``unimplemented!()`告诉编译器该函数尚未实现,其实主要帮助我们快速完成主要代码,回头可以通过搜索这些标记来完成次要代码,类似的还有`todo!()`.当代码执行到这种语句使,编译器会直接报错,你可以反注释`read(&mut f1, &mut vec![]);`这行,然后再尝试运行程序
`read`函数也非常有趣,它返回一个`!`,这个表明该函数是一个发散函数,不会返回任何值,包括`()``unimplemented!()`告诉编译器该函数尚未实现,其实主要帮助我们快速完成主要代码,回头可以通过搜索这些标记来完成次要代码,类似的还有`todo!()`,当代码执行到这种未实现的地方时,程序会直接报错: 你可以反注释`read(&mut f1, &mut vec![]);`这行,然后再观察下结果
同时,从代码设计角度来看,关于文件操作的类型和函数散落的到处都是,特别是当文件属性和相关的操作多了后,更是难以管理,而且`open(&mut f1)`也远没有`f1.open()`好,因此这就是基本类型的局限性:**无法从更高的抽象层次去简化代码**。
同时,从代码设计角度来看,假如关于文件操作的类型和函数散落的到处都是,是难以管理和使用的。而且`open(&mut f1)`也远没有`f1.open()`好,因此这就是基本类型的局限性:**无法从更高的抽象层次去简化代码**。
接下来,我们将引入结构体这个高级数据结构,来看看怎么样更好的解决这类问题,开始之前,先来看看何为`元组`.
接下来,我们将引入一个高级数据结构 - 结构体`struct`,来看看怎么样更好的解决这类问题。 开始之前,先来看看何为`元组`.

@ -1,7 +1,6 @@
# 字符串
在其他语言,字符串往往是送分题,因为实在是太简单了,例如`"hello, world"`就是字符串章节的几乎全部内容了对吧如果你带着这样的想法来学Rust
我保证,绝对会栽跟头,**因此这一章大家一定要重视仔细阅读这里有很多其它Rust书籍中没有的内容**。
在其他语言,字符串往往是送分题,因为实在是太简单了,例如`"hello, world"`就是字符串章节的几乎全部内容了对吧如果你带着这样的想法来学Rust我保证绝对会栽跟头**因此这一章大家一定要重视仔细阅读这里有很多其它Rust书籍中没有的内容**。
首先来看段很简单的代码:
```rust
@ -42,21 +41,19 @@ Bingo果然报错了编译器提示`greet`函数需要一个`String`类型
```rust
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
let hello = &s[0..5];
let world = &s[6..11];
```
`hello`没有引用整个`String s`,而是引用了`s`的一部分内容,通过`[0..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%;" />
<span class="caption">String切片引用了另一个`String`的一部分</span>
在使用Rust的`..`区间(range)语法时如果你想从索引0开始可以使用如下的方式这两个是等效的
在使用Rust的`..`[range序列](../base-type/numbers.md#序列(Range))语法时如果你想从索引0开始可以使用如下的方式这两个是等效的
```rust
let s = String::from("hello");
@ -93,7 +90,7 @@ let slice = &s[..];
>因为我们只取`s`字符串的前两个字节,但是一个中文占用三个字节,因此没有落在边界处,也就是连`中`字都取不完整,此时程序会直接崩溃退出,如果改成`&a[0..3]`,则可以正常通过编译.
> 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点, 关于该如何操作utf8字符串参见[这里](#操作UTF8字符串)
字符串切片的类型标是`&str`,因此我们可以这样申明一个函数,输入`String`类型,返回它的切片: `fn first_word(s: &String) -> &str `.
字符串切片的类型标是`&str`,因此我们可以这样申明一个函数,输入`String`类型,返回它的切片: `fn first_word(s: &String) -> &str `.
有了切片就可以写出这样的安全代码:
```rust

@ -1,14 +1,14 @@
# 结构体
在上一节,我们提到需要一个更高级的数据结构来帮助我们更好的抽象问题,结构体`strct`恰恰就是这样的复合数据结构,它是由其它数据类型组合而来, 其它语言也有类似的数据结构,不过可能有不同的名称,例如`object`、`record`等。
上一节中提到需要一个更高级的数据结构来帮助我们更好的抽象问题,结构体`strct`恰恰就是这样的复合数据结构,它是由其它数据类型组合而来 其它语言也有类似的数据结构,不过可能有不同的名称,例如`object`、`record`等。
结构体跟之前讲过的[元组](../base-type/tuple.md)有些相像:都是由多种类型组合而成。但是与元组不同的是,结构体可以为内部的每个字段起一个富有含义的名称。因此结构体更加灵活更加强大,你无需依赖这些字段的顺序来访问和解析它们。
结构体跟之前讲过的[元组](./tuple.md)有些相像:都是由多种类型组合而成。但是与元组不同的是,结构体可以为内部的每个字段起一个富有含义的名称。因此结构体更加灵活更加强大,你无需依赖这些字段的顺序来访问和解析它们。
## 结构体语法
天下无敌的剑士往往也因为他有一炳无双之剑,既然结构体这么强大,那么我们就需要给它配套一套强大的语法,让用户能更好的驾驭。
#### 定义结构体
定义结构体有几部分组成:
一个结构体有几部分组成:
- 通过关键字`struct`定义
- 一个清晰明确的结构体`名称`
- 数个具名的结构体`字段`
@ -25,7 +25,7 @@ struct User {
该结构体名称是`User`拥有4个具名的字段且每个字段都有对应的类型声明例如`username`代表了用户名,是一个可变的`String`类型。
#### 创建结构体实例
为了使用上述结构体,我们需要创建`User`结构体的`实例`
为了使用上述结构体,我们需要创建`User`结构体的**实例**
```rust
let user1 = User {
email: String::from("someone@example.com"),
@ -35,11 +35,11 @@ struct User {
};
```
有几点值得注意:
1. 初始化实例时,需要为每个字段都进行初始化
1. 初始化实例时,需要为**每个字段**都进行初始化
2. 初始化时的字段顺序无需按照定义的顺序来
#### 访问结构体字段
通过`.`操作符即可访问结构体实例内部的字段值,并且也可以修改它们:
通过`.`操作符即可访问结构体实例内部的字段值,也可以修改它们:
```rust
let mut user1 = User {
email: String::from("someone@example.com"),
@ -53,7 +53,7 @@ struct User {
需要注意的是必须要将整个结构体都声明为可变的才能修改它Rust不允许单独将某个字段标记为可变: `let mut user1 = User {...}`.
#### 简化结构体创建
先看以下这个函数:
下面的函数类似一个构建函数,返回了`User`结构体的实例:
```rust
fn build_user(email: String, username: String) -> User {
User {
@ -64,7 +64,7 @@ fn build_user(email: String, username: String) -> User {
}
}
```
它接收两个字符串参数:`email`和`username`,然后使用它们来创建一个`User`结构体,并且返回。可以注意到这两行:`email: email`和`username: username`,非常的扎眼,因为实在太啰嗦了如果你从typscript过来肯定会鄙视Rust一番不过好在它也不是无可救药:
它接收两个字符串参数:`email`和`username`,然后使用它们来创建一个`User`结构体,并且返回。可以注意到这两行:`email: email`和`username: username`,非常的扎眼,因为实在有些啰嗦如果你从typscript过来肯定会鄙视Rust一番不过好在它也不是无可救药:
```rust
fn build_user(email: String, username: String) -> User {
User {
@ -89,7 +89,7 @@ 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"),
@ -104,10 +104,10 @@ fn build_user(email: String, username: String) -> User {
>
> 聪明的读者肯定要发问了:明明有三个字段进行了自动赋值,为何只有`username`发生了所有权转移?
>
> 仔细回想一下[所有权](../ownership/ownership.md)那一节的内容我们提到了Copy特征实现了Copy特征的类型无需所有权转移可以直接在赋值时进行
> 仔细回想一下[所有权](../ownership/ownership.md#拷贝(浅拷贝))那一节的内容我们提到了Copy特征实现了Copy特征的类型无需所有权转移可以直接在赋值时进行
> 数据拷贝,其中`bool`和`u64`类型就实现了`Copy`特征,因此`active`和`sign_in_count`字段在赋值给user2时仅仅发生了拷贝而不是所有权转移.
>
> 值的注意的是:`username`所有权被转移给了`user2`,导致了`user1`无法再被使用,但是并不代表`user1`内部的字段不能被急需使用,例如:
> 值的注意的是:`username`所有权被转移给了`user2`,导致了`user1`无法再被使用,但是并不代表`user1`内部的字段不能被继续使用,例如:
```rust
let user1 = User {
@ -132,7 +132,7 @@ fn build_user(email: String, username: String) -> User {
## 结构体的内存排列
先看以下代码:
看以下代码:
```rust
#[derive(Debug)]
struct File {
@ -246,4 +246,4 @@ help: consider introducing a named lifetime parameter
```
未来在[生命周期](../../advance/lifetime.md)中会讲到如何修复这个问题以便在结构体中存储引用,不过在那之前,我们会避免在结构体中使用引用类型。
未来在[生命周期](../../advance/lifetime/basic.md)中会讲到如何修复这个问题以便在结构体中存储引用,不过在那之前,我们会避免在结构体中使用引用类型。

@ -1,6 +1,6 @@
# 元组
元组也是复合类型的一种,因此它是由多种类型组合到一起形成的。元组的长度是固定的,且在声明后,无法进行伸缩
元组是由多种类型组合到一起形成的,因此它是复合类型,元组的长度是固定的
通过以下语法可以创建一个元组:
```rust
@ -11,7 +11,7 @@ fn main() {
变量`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);
@ -62,7 +62,8 @@ fn calculate_length(s: String) -> (String, usize) {
}
```
`calculate_length`函数接收`s1`字符串的所有权,然后计算字符串的长度,接着把字符串所有权和字符串长度返回给`s2`和`len`变量。
`calculate_length`函数接收`s1`字符串的所有权,然后计算字符串的长度,接着把字符串所有权和字符串长度返回给`s2`和`len`变量。
对于其他语言元组可以用来声明一个3D点例如`Point(10,20,30)`虽然使用Rust元组也可以做到`(10,20,30)`,但是这样写有个非常重大的缺陷:
不具备任何清晰的含义,在下一章节中,会提到一种`元组结构体`,可以解决这个问题。
**不具备任何清晰的含义**,在下一章节中,会提到一种`元组结构体`,可以解决这个问题。

@ -1,6 +1,6 @@
# 引用与借用
上节中提到,如果仅仅是所有权转移,会让程序变得复杂,那能否像其它编程语言一样,使用某个变量的指针或者引用呢?答案是有的。
上节中提到,如果仅仅是支持所有权转移,那会让程序变得复杂。 能否像其它编程语言一样,使用某个变量的指针或者引用呢?答案是有的。
Rust通过`借用(Borrowing)`这个概念来达成上述的目的: **获取变量的引用,称之为借用(borrowing)**。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来,当使用完毕后,也必须要物归原主.
@ -54,14 +54,13 @@ fn calculate_length(s: &String) -> usize {
```
能注意到两点:
1. 无需再通过函数参数来传入所有权,通过函数返回来传出所有权,代码更加简洁
1. 无需像上章一样:先通过函数参数传入所有权,然后再通过函数返回来传出所有权,代码更加简洁
2. `calculate_length`的参数`s`类型从`String`变为`&String`
这里,`&`符号即是引用,它们允许你使用值,但是不获取所有权,如图所示:
<img alt="&String s pointing at String s1" src="/img/borrowing-01.svg" class="center" />
<span class="caption">图:`&String s` 指向 `String s1`的示意图</span>
`&s1`语法,让我们创建一个**指向s1的引用**,但是并不拥有它。因为并不拥有这个值,当引用离开作用域后,其指向的值也不会被丢弃。
通过`&s1`语法,我们创建了一个**指向s1的引用**,但是并不拥有它。因为并不拥有这个值,当引用离开作用域后,其指向的值也不会被丢弃。
同理,函数`calculate_length`使用`&`来表明参数`s`的类型是一个引用:
```rust
@ -71,7 +70,7 @@ fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
// 所以什么也不会发生
```
人总是贪心的,可以摸女孩手了,就想着摸摸胳膊(读者中的老司机表示,这个流程完全不对),因此光借用已经满足不了我们了,如果尝试修改借用的变量呢?
人总是贪心的,可以拉女孩小手了,就想着抱抱柔软的身子(读者中的某老司机表示,这个流程完全不对),因此光借用已经满足不了我们了,如果尝试修改借用的变量呢?
```rust
fn main() {
let s = String::from("hello");
@ -84,7 +83,7 @@ fn change(some_string: &String) {
}
```
很不幸,胳膊你没摸到, 哦口误,你修改错了:
很不幸,妹子你没抱到, 哦口误,你修改错了:
```console
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
@ -118,7 +117,7 @@ fn change(some_string: &mut String) {
##### 可变引用同时只能存在一个
不过可变引用并不是随心所欲、想用就用的,它有一个很大的限制:同一作用域,特定数据只能一个可变引用:
不过可变引用并不是随心所欲、想用就用的,它有一个很大的限制:同一作用域,特定数据只能一个可变引用:
```rust
let mut s = String::from("hello");
@ -143,6 +142,7 @@ error[E0499]: cannot borrow `s` as mutable more than once at a time 同一时间
```
这段代码出错的原因在于,第一个可变借用`r1`必须要持续到最后一次使用的位置`println!`,在`r1`创建和最后一次使用之间,我们又尝试创建第二个引用`r2`。
对于新手来说,这个特性绝对是一大拦路虎,也是新人们谈之色变的编译器`borrow checker`特性之一不过各行各业都一样限制往往是出于安全的考虑Rust也一样。
这种限制的好处就是使Rust在编译期就避免数据竞争数据竞争可由以下行为造成
@ -150,9 +150,9 @@ error[E0499]: cannot borrow `s` as mutable more than once at a time 同一时间
- 至少有一个指针被用来写入数据
- 没有同步数据访问的机制
数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码!
数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复。而Rust避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码!
很多时候,大括号可以帮我们解决一些问题,通过手动限制变量的作用域:
很多时候,大括号可以帮我们解决一些编译不通过的问题,通过手动限制变量的作用域:
```rust
let mut s = String::from("hello");
@ -178,7 +178,7 @@ println!("{}, {}, and {}", r1, r2, r3);
```
错误如下:
```rust
```console
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable 无法借用可变`s`因为它已经被借用了不可变
--> src/main.rs:6:14
|
@ -194,7 +194,7 @@ error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immuta
其实这个也很好理解,借用了不可变的用户,肯定不希望他借用的东西,被另外一个人莫名其妙改变了。多个不可变借用被允许是因为没有人会去试图修改数据,然后导致别人的数据被污染。
> 注意,引用的作用域从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号`}`
> 注意,引用的作用域s从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号`}`
Rust的编译器一直在优化早期的时候引用的作用域跟变量作用域是一致的这对日常使用带来了很大的困扰你必须非常小心的去安排可变、不可变变量的借用免得无法通过编译例如以下代码
```rust
@ -225,11 +225,9 @@ fn main() {
所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
让我们尝试创建一个悬垂引用Rust 会通过一个编译时错误来避免
让我们尝试创建一个悬垂引用Rust会抛出一个编译时错误
<span class="filename">文件名: src/main.rs</span>
```rust,ignore,does_not_compile
```rust
fn main() {
let reference_to_nothing = dangle();
}
@ -265,10 +263,10 @@ this function's return type contains a borrowed value, but there is no value for
该函数返回了一个借用的值,但是已经找不到它所借用值的来源
```
让我们仔细看看我们的 `dangle` 代码的每一步到底发生了什么:
仔细看看 `dangle` 代码的每一步到底发生了什么:
```rust,ignore,does_not_compile
```rust
fn dangle() -> &String { // dangle 返回一个字符串的引用
let s = String::from("hello"); // s 是一个新字符串
@ -278,9 +276,9 @@ fn dangle() -> &String { // dangle 返回一个字符串的引用
// 危险!
```
因为 `s` 是在 `dangle` 函数内创建的,当 `dangle` 的代码执行完毕后,`s` 将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的 `String`,这可不对!Rust 不会允许我们这么做。
因为 `s` 是在 `dangle` 函数内创建的,当 `dangle` 的代码执行完毕后,`s` 将被释放, 但是此时我们又尝试去返回它的引用。这意味着这个引用会指向一个无效的 `String`,这可不对!
这里的解决方法是直接返回 `String`
其中一个很好的解决方法是直接返回 `String`
```rust
fn no_dangle() -> String {
@ -290,7 +288,7 @@ fn no_dangle() -> String {
}
```
这样就没有任何错误了。所有权被移动出去,所以没有值被释放
这样就没有任何错误了,最终`String`的**所有权被转移给外面的调用者**
## 借用规则总结

@ -0,0 +1 @@
# 优化编译速度
Loading…
Cancel
Save