Merge pull request #352 from AllanDowney/patch-2

Update: unified format
pull/358/head
Sunface 3 years ago committed by GitHub
commit dd363dfb83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -19,7 +19,7 @@ fn main() {
}
```
数组语法跟`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];`,不过老板可能很喜欢你的这种疯狂编程的状态。
@ -84,7 +84,7 @@ fn main() {
}
```
使用`cargo run`来运行代码因为数组只有5个元素如果我们试图输入`5`去访问第`6`个元素,则会访问到不存在的数组元素,最终程序会崩溃退出:
使用 `cargo run` 来运行代码,因为数组只有 5 个元素,如果我们试图输入 `5` 去访问第 6 个元素,则会访问到不存在的数组元素,最终程序会崩溃退出:
```console
Please enter an array index.
5
@ -94,7 +94,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
这就是数组访问越界,访问了数组中不存在的元素,导致 Rust 运行时错误。程序因此退出并显示错误消息,未执行最后的 `println!` 语句。
当你尝试使用索引访问元素时Rust 将检查你指定的索引是否小于数组长度。如果索引大于或等于数组长度Rust会出现 panic。这种检查只能在运行时进行比如在上面这种情况下编译器无法在编译期知道用户运行代码时将输入什么值。
当你尝试使用索引访问元素时Rust 将检查你指定的索引是否小于数组长度。如果索引大于或等于数组长度Rust会出现 ***panic***。这种检查只能在运行时进行,比如在上面这种情况下,编译器无法在编译期知道用户运行代码时将输入什么值。
这种就是 Rust 的安全特性之一。在很多系统编程语言中,并不会检查数组越界问题,你会访问到无效的内存地址获取到一个风马牛不相及的值,最终导致在程序逻辑上出现大问题,而且这种问题会非常难以检查。

@ -20,7 +20,7 @@ enum PokerSuit {
**枚举类型是一个类型,它会包含所有可能的枚举成员, 而枚举值是该类型中的具体某个成员的实例。**
## 枚举值
现在来创建`PokerSuit`枚举类型的两个成员实例:
现在来创建 `PokerSuit` 枚举类型的两个成员实例:
```rust
let heart = PokerSuit::Hearts;
let diamond = PokerSuit::Diamonds;
@ -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--
@ -142,7 +142,7 @@ fn main() {
}
```
该枚举类型代表一条消息,它包含四个不同的成员:
该枚举类型代表一条消息,它包含四个不同的成员
- `Quit` 没有任何关联数据
- `Move` 包含一个匿名结构体
- `Write` 包含一个 `String` 字符串
@ -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,7 +207,7 @@ 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` 下的卧龙凤雏。
@ -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> {

@ -36,4 +36,4 @@ fn main() {
同时,从代码设计角度来看,关于文件操作的类型和函数应该组织在一起,散落得到处都是,是难以管理和使用的。而且通过 `open(&mut f1)` 进行调用,也远没有使用 `f1.open()` 来调用好,这就体现出了只使用基本类型的局限性:**无法从更高的抽象层次去简化代码**。
接下来,我们将引入一个高级数据结构 - 结构体`struct`,来看看复合类型是怎样更好的解决这类问题。 开始之前先来看看Rust的重点也是难点字符串`String`和`&str`。
接下来,我们将引入一个高级数据结构 —— 结构体 `struct`,来看看复合类型是怎样更好的解决这类问题。 开始之前,先来看看 Rust 的重点也是难点:字符串 `String` `&str`

@ -87,10 +87,10 @@ let slice = &s[..];
> let a = &s[0..2];
> println!("{}",a);
>```
>因为我们只取`s`字符串的前两个字节,但是一个中文占用三个字节,因此没有落在边界处,也就是连`中`字都取不完整,此时程序会直接崩溃退出,如果改成`&a[0..3]`,则可以正常通过编译.
>因为我们只取 `s` 字符串的前两个字节,但是一个中文占用三个字节,因此没有落在边界处,也就是连 `中` 字都取不完整,此时程序会直接崩溃退出,如果改成 `&a[0..3]`,则可以正常通过编译。
> 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点, 关于该如何操作 UTF8 字符串,参见[这里](#操作UTF8字符串)
字符串切片的类型标识是`&str`,因此我们可以这样声明一个函数,输入`String`类型,返回它的切片: `fn first_word(s: &String) -> &str `.
字符串切片的类型标识是 `&str`,因此我们可以这样声明一个函数,输入 `String` 类型,返回它的切片: `fn first_word(s: &String) -> &str `
有了切片就可以写出这样的安全代码:
```rust
@ -124,7 +124,7 @@ error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immuta
从上述代码可以看出Rust 不仅让我们的 `API` 更加容易使用,而且也在编译期就消除了大量错误!
#### 其它切片
因为切片是对集合的部分引用,因此不仅仅字符串有切片,其它集合类型也有,例如数组:
因为切片是对集合的部分引用,因此不仅仅字符串有切片,其它集合类型也有,例如数组
```rust
let a = [1, 2, 3, 4, 5];
@ -152,17 +152,17 @@ let s: &str = "Hello, world!";
## 什么是字符串?
顾名思义,字符串是由字符组成的连续集合,但是在上一节中我们提到过,**Rust中的字符是Unicode类型因此每个字符占据4个字节内存空间但是在字符串中不一样字符串是UTF8编码也就是字符所占的字节数是变化的(1-4)**,这样有助于大幅降低字符串所占用的内存空间.
顾名思义,字符串是由字符组成的连续集合,但是在上一节中我们提到过,**Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF8 编码,也就是字符所占的字节数是变化的(1 - 4)**,这样有助于大幅降低字符串所占用的内存空间
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,7 +216,7 @@ 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` 操作,下面来展开讲讲。
@ -259,12 +259,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"` 中的每个字母在 UTF8 编码中仅占用 1 个字节,但是对于下面的代码呢?
```rust
let hello = String::from("中国人");
```
如果问你该字符串多长,你可能会说`3`,但是实际上是`9`个字节的长度因为每个汉字在UTF8中的长度是`3`个字节,因此这种情况下对`hello`进行索引
访问`&hello[0]`没有任何意义,因为你取不到`中`这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的返回值。
如果问你该字符串多长,你可能会说 `3`,但是实际上是 `9` 个字节的长度,因为每个汉字在 UTF8 中的长度是 `3` 个字节,因此这种情况下对 `hello` 进行索引,访问 `&hello[0]` 没有任何意义,因为你取不到 `中` 这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的返回值。
#### 字符串的不同表现形式
现在看一下用梵文写的字符串 `“नमस्ते”`, 它底层的字节数组如下形式:
@ -299,7 +298,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
这里提示的很清楚,我们索引的字节落在了 `中` 字符的内部,这种返回没有任何意义。
因此在通过索引区间来访问字符串时,需要格外的小心,一不注意,就会导致你程序的崩溃!
因此在通过索引区间来访问字符串时,**需要格外的小心**,一不注意,就会导致你程序的崩溃!
## 操作UTF8字符串
前文提到了几种使用 UTF8 字符串的方式,下面来一一说明。
@ -339,10 +338,10 @@ for b in "中国人".bytes() {
```
#### 获取子串
想要准确的从UTF8字符串中获取子串是较为复杂的事情例如想要从`holla中国人नमस्ते`这种变长的字符串中取出某一个子串,使用标准库你是做不到的
想要准确的从UTF8字符串中获取子串是较为复杂的事情例如想要从 `holla中国人नमस्ते` 这种变长的字符串中取出某一个子串,使用标准库你是做不到的
你需要在 `crates.io` 上搜索 `utf8` 来寻找想要的功能。
可以考虑尝试下这个库:[utf8_slice](https://crates.io/crates/utf8_slice).
可以考虑尝试下这个库:[utf8_slice](https://crates.io/crates/utf8_slice)
@ -350,17 +349,17 @@ for b in "中国人".bytes() {
## 字符串深度剖析
那么问题来了,为啥 `String` 可变,而字符串字面值 `str` 却不可以?
就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串的不可变性。不幸的是,我们不能为了获得这种性能,而把每一个在编译时大小未知的文本都放进内存中(你也做不到!),因为有的字符串是在程序运行得过程中动态生成的。
就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串的不可变性。不幸的是,我们不能为了获得这种性能,而把每一个在编译时大小未知的文本都放进内存中(你也做不到!),因为有的字符串是在程序运行得过程中动态生成的。
对于 `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` 函数并不陌生

@ -37,7 +37,7 @@ struct User {
有几点值得注意:
1. 初始化实例时,**每个字段**都需要进行初始化
2. 初始化时的字段顺序不需要和结构体定义时的顺序一致
2. 初始化时的字段顺序**不需要**和结构体定义时的顺序一致
#### 访问结构体字段
通过 `.` 操作符即可访问结构体实例内部的字段值,也可以修改它们:
@ -51,7 +51,7 @@ struct User {
user1.email = String::from("anotheremail@example.com");
```
需要注意的是必须要将结构体实例声明为可变的才能修改其中的字段Rust不支持将某个结构体某个字段标记为可变.
需要注意的是必须要将结构体实例声明为可变的才能修改其中的字段Rust 不支持将某个结构体某个字段标记为可变
#### 简化结构体创建
下面的函数类似一个构建函数,返回了 `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,7 +76,7 @@ fn build_user(email: String, username: String) -> User {
}
}
```
如上所示,当函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化,跟`typescript`中一模一样.
如上所示,当函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化,跟 TypeScript 中一模一样。
#### 结构体更新语法
@ -90,7 +90,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"),
@ -101,14 +101,14 @@ fn build_user(email: String, username: String) -> User {
`..` 语法表明凡是我们没有显示声明的字段,全部从 `user1` 中自动获取。需要注意的是 `..user1` 必须在结构体的尾部使用。
> 结构体更新语法跟赋值语句`=`非常相像,因此在上面代码中,`user1`的部分字段所有权被转移到`user2`中:`username`字段发生了所有权转移,作为结果,`user1`无法再被使用。
> 结构体更新语法跟赋值语句 `=` 非常相像,因此在上面代码中,`user1` 的部分字段所有权被转移到 `user2` 中:`username` 字段发生了所有权转移,作为结果,`user1` 无法再被使用。
>
> 聪明的读者肯定要发问了:明明有三个字段进行了自动赋值,为何只有 `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)]
@ -164,9 +164,9 @@ println!("{:?}", user1);
上面定义的 `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)
@ -182,9 +182,9 @@ println!("{:?}", user1);
元组结构体在你希望有一个整体名称,但是又不关心里面字段的名称时将非常有用。例如上面的 `Point` 元组结构体众所周知3D点是 `(x, y, z)` 形式的坐标点,因此我们无需再为内部的字段逐一命名为:`x`, `y`, `z`
## 元结构体(Unit-like Struct)
还记得之前讲过的基本没啥用的[单元类型](../base-type/char-bool.md#单元类型)吧? 元结构体就跟它很像,没有任何字段和属性,但是好在,它还挺有用。
还记得之前讲过的基本没啥用的[单元类型](../base-type/char-bool.md#单元类型)吧元结构体就跟它很像,没有任何字段和属性,但是好在,它还挺有用。
如果你定义一个类型,但是不关心该类型的内容, 只关心它的行为时,就可以使用`元结构体`:
如果你定义一个类型,但是不关心该类型的内容, 只关心它的行为时,就可以使用 `元结构体`
```rust
struct AlwaysEqual;
@ -199,9 +199,9 @@ impl SomeTrait for AlwaysEqual {
## 结构体数据的所有权
在之前的`User` 结构体的定义中,有一处细节:我们使用了自身拥有所有权的 `String` 类型而不是基于引用的`&str` 字符串切片类型。这是一个有意而为之的选择因为我们想要这个结构体拥有它所有的数据,而不是从其它地方借用数据。
在之前的 `User` 结构体的定义中,有一处细节:我们使用了自身拥有所有权的 `String` 类型而不是基于引用的 `&str` 字符串切片类型。这是一个有意而为之的选择因为我们想要这个结构体拥有它所有的数据,而不是从其它地方借用数据。
你也可以让`User`结构体从其它对象借用数据,不过这么做,就需要引入**生命周期**这个新概念(也是一个复杂的概念),简而言之,生命周期能确保结构体的作用范围要比它所借用的数据的作用范围要小。
你也可以让 `User` 结构体从其它对象借用数据,不过这么做,就需要引入[生命周期lifetimes](../../advance/lifetime/basic.md)这个新概念(也是一个复杂的概念),简而言之,生命周期能确保结构体的作用范围要比它所借用的数据的作用范围要小。
总之,如果你想在结构体中使用一个引用,就必须加上生命周期,否则就会报错:

@ -44,7 +44,7 @@ fn main() {
和其它语言的数组、字符串一样,元组的索引从 0 开始。
### 元组的使用示例
元组在函数返回值场景很常用,例如下面的代码,可以使用元组返回多个值:
元组在函数返回值场景很常用,例如下面的代码,可以使用元组返回多个值
```rust
fn main() {

@ -27,9 +27,9 @@ Rust跟其它语言不一样你无法看了一遍语法然后就能上手
## 千万别从链表或图开始练手
CS 课程中我们会学习大量的常用数据结构和算法,因此大家都养成了一种好习惯:学习一门新语言,先用它写个链表或图试试。
我的天在Rust中千万别这么干你是在扼杀自己之前的努力因为不像其它语言链表在Rust中简直是地狱一般的难度我见过太多英雄好汉难过链表关最终黯然退幕。我不希望正在阅读此文的你也成为其中一个 :
我的天,在 Rust **千万别这么干**,你是在扼杀自己之前的努力!因为不像其它语言,链表在 Rust 中简直是地狱一般的难度,我见过太多英雄好汉难过链表关,最终黯然退幕。我不希望正在阅读此文的你也成为其中一个 :
这些自引用类型的数据结构(包含了字段,该字段又引用了自身),它们是恶魔,它们不仅仅在蹂躏着新手,还在折磨着老手,有意思的是,它们的难恰恰是Rust的优点导致的无gc也无手动内存管理,内存安全。
这些自引用类型的数据结构(包含了字段,该字段又引用了自身),它们是恶魔,它们不仅仅在蹂躏着新手,还在折磨着老手,有意思的是,它们的难恰恰是 Rust 的优点导致的:无 GC 也无手动内存管理,内存安全。
这两点的实现并不是凭空产生的,而是通过 Rust 一套非常强大、优美的机制提供了支持,这些机制一旦你学到,就会被它巧妙的构思和设计而征服,进而被 Rust 深深吸引!但是一切选择都有利弊,这种机制的弊端就在于实现链表这种数据结构时,会变得非常非常复杂。

Loading…
Cancel
Save