Merge pull request #117 from 1132719438/main

Some fixes in basic chapter
pull/126/head
Sunface 3 years ago committed by GitHub
commit 36deb1b065
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -19,7 +19,7 @@ fn main() {
} }
``` ```
数组语法跟`javascript`很像,也跟大多数编程语言很像。由于它的元素类型大小固定,且长度也是固定,因此**数组是存储在栈上**,性能也会非常优秀。与此对应,动态数组`Vector`是存储在堆上,因此长度可以动态改变。当你不确定是使用数组还是动态数组时,那就应该使用后者,具体见[动态数组Vector]一章. 数组语法跟`javascript`很像,也跟大多数编程语言很像。由于它的元素类型大小固定,且长度也是固定,因此**数组是存储在栈上**,性能也会非常优秀。与此对应,动态数组`Vector`是存储在堆上,因此长度可以动态改变。当你不确定是使用数组还是动态数组时,那就应该使用后者,具体见[动态数组Vector](../collections/vector.md)一章.
举个例子,在需要知道一年中各个月份名称的程序中,你很可能希望使用的是数组而不是动态数组。因为月份是固定的,它总是包含 12 个元素: 举个例子,在需要知道一年中各个月份名称的程序中,你很可能希望使用的是数组而不是动态数组。因为月份是固定的,它总是包含 12 个元素:
```rust ```rust
@ -35,7 +35,7 @@ let a: [i32; 5] = [1, 2, 3, 4, 5];
还可以使用下面的语法初始化一个**某个值重复出现N次的数组**: 还可以使用下面的语法初始化一个**某个值重复出现N次的数组**:
```rust ```rust
let a: = [3; 5]; let a = [3; 5];
``` ```
`a`数组包含`5`个元素,这些元素的初始化值为`3`,聪明的读者已经发现,这种语法跟数组类型的声明语法其实是保持一致的:`[3;5]` 和`[类型;长度]`. `a`数组包含`5`个元素,这些元素的初始化值为`3`,聪明的读者已经发现,这种语法跟数组类型的声明语法其实是保持一致的:`[3;5]` 和`[类型;长度]`.
@ -149,7 +149,7 @@ fn main() {
做个总结,数组虽然很简单,但是其实还是存在几个要注意的点: 做个总结,数组虽然很简单,但是其实还是存在几个要注意的点:
- **数组类型容易跟数组切片混淆**,[T;n]描述了一个数组的类型,而[T]描述了切片的类型, 因为切片是运行期的数据结构,因此它不具备编译器的长度,因此不能用[T;n]的形式去描述 - **数组类型容易跟数组切片混淆**,[T;n]描述了一个数组的类型,而[T]描述了切片的类型, 因为切片是运行期的数据结构,因此它不具备编译器的长度,因此不能用[T;n]的形式去描述
- `[u8; 3]`和`[u8;4]是不同的类型`,数组的长度也是类型的一部分 - `[u8; 3]`和`[u8; 4]`是不同的类型,数组的长度也是类型的一部分
- **在实践中,使用最多的是数组切片[T]**,我们往往通过引用的方式去使用`&[T]`,因为后者有固定的类型大小. - **在实践中,使用最多的是数组切片[T]**,我们往往通过引用的方式去使用`&[T]`,因为后者有固定的类型大小.

@ -1,6 +1,6 @@
# 枚举 # 枚举
枚举(enum或enumeration)允许你通过列举可能的成员来定义一个**`枚举类型`**,例如扑克牌花色: 枚举(enum或enumeration)允许你通过列举可能的成员来定义一个**枚举类型**,例如扑克牌花色:
```rust ```rust
enum PokerSuit { enum PokerSuit {
Clubs, Clubs,
@ -122,7 +122,7 @@ enum IpAddr {
V6(Ipv6Addr), V6(Ipv6Addr),
} }
``` ```
该例子跟我们之前的扑克牌很像,只不过枚举成员包含的类型更复杂了,变成了结构体:分别通过`Ipv4Addr`和`Ipv4Addr`来定义两种不同的IP数据。 该例子跟我们之前的扑克牌很像,只不过枚举成员包含的类型更复杂了,变成了结构体:分别通过`Ipv4Addr`和`Ipv6Addr`来定义两种不同的IP数据。
从这些例子可以看出,**任何类型的数据都可以放入枚举成员中**: 例如字符串、数值、结构体甚至另一个枚举。 从这些例子可以看出,**任何类型的数据都可以放入枚举成员中**: 例如字符串、数值、结构体甚至另一个枚举。
@ -193,7 +193,7 @@ enum Websocket {
## Option枚举用于处理空值 ## Option枚举用于处理空值
在其它编程语言中,往往都有一个`null`关键字,该关键字用于表明一个变量当前的值为空(不是零值例如整形的零值是0),也就是不存在值。当你对这些`null`进行操作时例如调用一个方法就会直接抛出null异常导致程序的崩溃因此我们在编程时需要格外的小心去处理这些`null`空值。 在其它编程语言中,往往都有一个`null`关键字,该关键字用于表明一个变量当前的值为空(不是零值例如整形的零值是0),也就是不存在值。当你对这些`null`进行操作时例如调用一个方法就会直接抛出null异常导致程序的崩溃因此我们在编程时需要格外的小心去处理这些`null`空值。
> Tony Hoarenull的发明者曾经说过非常有名的话 > Tony Hoarenull的发明者曾经说过一段非常有名的话
> >
> 我称之为我十亿美元的错误。当时,我在使用一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过在设计过程中,我未能抵抗住诱惑,引入了空引用的概念,因为它非常容易实现。就是因为这个决策,引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数十亿美元的苦痛和伤害。 > 我称之为我十亿美元的错误。当时,我在使用一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过在设计过程中,我未能抵抗住诱惑,引入了空引用的概念,因为它非常容易实现。就是因为这个决策,引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数十亿美元的苦痛和伤害。

@ -302,7 +302,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
因此在通过索引区间来访问字符串时,需要格外的小心,一不注意,就会导致你程序的崩溃! 因此在通过索引区间来访问字符串时,需要格外的小心,一不注意,就会导致你程序的崩溃!
## 操作UTF8字符串 ## 操作UTF8字符串
前文提到了几使用UTF8字符串的方式下面来一一说明。 前文提到了几使用UTF8字符串的方式下面来一一说明。
#### 字符 #### 字符
如果你想要以Unicode字符的方式遍历字符串最好的办法是使用`chars`方法,例如: 如果你想要以Unicode字符的方式遍历字符串最好的办法是使用`chars`方法,例如:
@ -342,7 +342,7 @@ for b in "中国人".bytes() {
想要准确的从UTF8字符串中获取子串是较为复杂的事情例如想要从`holla中国人नमस्ते`这种变长的字符串中取出某一个子串,使用标准库你是做不到的, 想要准确的从UTF8字符串中获取子串是较为复杂的事情例如想要从`holla中国人नमस्ते`这种变长的字符串中取出某一个子串,使用标准库你是做不到的,
你需要在`crates.io`上搜索`utf8`来寻找想要的功能。 你需要在`crates.io`上搜索`utf8`来寻找想要的功能。
可以考虑尝试下这个库:[utf8 slice](https://crates.io/crates/utf8_slice). 可以考虑尝试下这个库:[utf8_slice](https://crates.io/crates/utf8_slice).
@ -358,7 +358,7 @@ for b in "中国人".bytes() {
其中第一个由`String::from`完成它创建了一个全新的String. 其中第一个由`String::from`完成它创建了一个全新的String.
重点来了,到了第二部分,就是百家齐放的环节,在有**垃圾回收GC**的语言中GC来负责标记并清除这些不再使用的内存对象这些都是自动完成无需开发者关心非常简单好用在无GC的语言是开发者手动去释放这些内存对象就像创建对象一样需要通过编写代码来完成因为未能正确释放对象造成的经济简直不可估量. 重点来了,到了第二部分,就是百家齐放的环节,在有**垃圾回收GC**的语言中GC来负责标记并清除这些不再使用的内存对象这些都是自动完成无需开发者关心非常简单好用在无GC的语言是开发者手动去释放这些内存对象就像创建对象一样需要通过编写代码来完成因为未能正确释放对象造成的结局简直不可估量.
对于Rust而言安全和性能是写到骨子里的核心特性使用GC牺牲了性能使用手动管理内存牺牲了安全那该怎么办为此Rust的开发者想出了一个无比惊艳的办法变量在离开作用域后就自动释放其占用的内存: 对于Rust而言安全和性能是写到骨子里的核心特性使用GC牺牲了性能使用手动管理内存牺牲了安全那该怎么办为此Rust的开发者想出了一个无比惊艳的办法变量在离开作用域后就自动释放其占用的内存:

@ -5,7 +5,7 @@
结构体跟之前讲过的[元组](./tuple.md)有些相像:都是由多种类型组合而成。但是与元组不同的是,结构体可以为内部的每个字段起一个富有含义的名称。因此结构体更加灵活更加强大,你无需依赖这些字段的顺序来访问和解析它们。 结构体跟之前讲过的[元组](./tuple.md)有些相像:都是由多种类型组合而成。但是与元组不同的是,结构体可以为内部的每个字段起一个富有含义的名称。因此结构体更加灵活更加强大,你无需依赖这些字段的顺序来访问和解析它们。
## 结构体语法 ## 结构体语法
天下无敌的剑士往往也因为他有一无双之剑,既然结构体这么强大,那么我们就需要给它配套一套强大的语法,让用户能更好的驾驭。 天下无敌的剑士往往也因为他有一无双之剑,既然结构体这么强大,那么我们就需要给它配套一套强大的语法,让用户能更好的驾驭。
#### 定义结构体 #### 定义结构体
一个结构体有几部分组成: 一个结构体有几部分组成:
@ -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`非常的扎眼因为实在有些啰嗦如果你从typescript过来肯定会鄙视Rust一番不过好在它也不是无可救药:
```rust ```rust
fn build_user(email: String, username: String) -> User { fn build_user(email: String, username: String) -> User {
User { User {
@ -184,7 +184,7 @@ struct AlwaysEqual;
let subject = AlwaysEqual; let subject = AlwaysEqual;
// 我们不关心为AlwaysEqual的字段数据只关心它的行为因此将它声明为元结构体然后再为它实现某个特征 // 我们不关心为AlwaysEqual的字段数据只关心它的行为因此将它声明为元结构体然后再为它实现某个特征
impl AlwaysEqual for SomeTrait { impl SomeTrait for AlwaysEqual {
} }
``` ```
@ -194,7 +194,7 @@ impl AlwaysEqual for SomeTrait {
在之前的`User` 结构体的定义中,我们使用了自身拥有所有权的 `String` 类型而不是基于引用的`&str` 字符串切片类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,而不是从其它地方借用数据。 在之前的`User` 结构体的定义中,我们使用了自身拥有所有权的 `String` 类型而不是基于引用的`&str` 字符串切片类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,而不是从其它地方借用数据。
你也可以让`User`结构体从其它对象借用数据,不过这么做,就需要引入**生命周期**这个新概念(也是一个复杂的概念),简而言之,生命周期能确保结构体的作用范围要比它所借用的数据的作用范围要 你也可以让`User`结构体从其它对象借用数据,不过这么做,就需要引入**生命周期**这个新概念(也是一个复杂的概念)简而言之,生命周期能确保结构体的作用范围要比它所借用的数据的作用范围要
总之,如果你想在结构体中使用一个引用,就必须加上生命周期,否则就会报错: 总之,如果你想在结构体中使用一个引用,就必须加上生命周期,否则就会报错:

@ -441,7 +441,7 @@ fn main() {
如果编译上面的例子,会得到下面的错误: 如果编译上面的例子,会得到下面的错误:
```text ```text
error: `..` can only be used once per tuple pattern // 每个元组模式只能使用一个`..'` error: `..` can only be used once per tuple pattern // 每个元组模式只能使用一个`..`
--> src/main.rs:5:22 --> src/main.rs:5:22
| |
5 | (.., second, ..) => { 5 | (.., second, ..) => {
@ -510,7 +510,7 @@ match x {
} }
``` ```
这个匹配条件表明此分支匹配 `x` 值为 `4`、`5` 或 `6` **同时** `y``true` 的情况。 这个匹配条件表明此分支匹配 `x` 值为 `4`、`5` 或 `6` **同时** `y``true` 的情况。
虽然在第一个分支中,`x`匹配了模式`4`,但是对于匹配守卫`if y`来说,因为`y`是`false`,因此该守卫条件的值永远是`false`,也意味着第一个分支永远无法被匹配. 虽然在第一个分支中,`x`匹配了模式`4`,但是对于匹配守卫`if y`来说,因为`y`是`false`,因此该守卫条件的值永远是`false`,也意味着第一个分支永远无法被匹配.

@ -44,6 +44,7 @@ match target {
}, },
_ => 表达式3 _ => 表达式3
} }
```
该形式清晰的说明了何为模式,何为模式匹配:将模式与`target`进行匹配,即为模式匹配,而模式匹配不仅仅局限于`match`,后面我们会详细阐述。 该形式清晰的说明了何为模式,何为模式匹配:将模式与`target`进行匹配,即为模式匹配,而模式匹配不仅仅局限于`match`,后面我们会详细阐述。
@ -117,7 +118,7 @@ enum Coin {
Quarter(UsState), // 25美分硬币 Quarter(UsState), // 25美分硬币
} }
``` ```
其中Coin::Quarter成员还存放了一个值美国的某个州因为在1999年到2008年间美国在25美分(Quater)硬币的背后为50个州印刷了不同的设计, 其它硬币都没有相关的设计。 其中`Coin::Quarter`成员还存放了一个值美国的某个州因为在1999年到2008年间美国在25美分(Quarter)硬币的背后为50个州印刷了不同的设计, 其它硬币都没有相关的设计。
接下来我们希望在模式匹配中获取到25美分硬币上刻印的州的名称 接下来我们希望在模式匹配中获取到25美分硬币上刻印的州的名称
```rust ```rust
@ -283,7 +284,7 @@ fn main() {
现在如果想对`v`进行过滤,只保留类型是`MyEnum::Foo`的元素,你可能想这么写: 现在如果想对`v`进行过滤,只保留类型是`MyEnum::Foo`的元素,你可能想这么写:
```rust ```rust
v.iter().filter(|x| x == MyEnum:::Foo); v.iter().filter(|x| x == MyEnum::Foo);
``` ```
但是,实际上这行代码会保存,因为你无法将`x`跟一个类型进行比较。好在,你可以使用`match`来完成,但是会导致代码更为啰嗦,是否有更简洁的方式?答案是使用`matches!`: 但是,实际上这行代码会保存,因为你无法将`x`跟一个类型进行比较。好在,你可以使用`match`来完成,但是会导致代码更为啰嗦,是否有更简洁的方式?答案是使用`matches!`:
@ -301,7 +302,7 @@ assert!(matches!(bar, Some(x) if x > 2));
``` ```
## 变量覆盖 ## 变量覆盖
无论是是`match`还是`if let`,他们都可以在模式匹配时覆盖掉老的值,绑定新的: 无论是是`match`还是`if let`,他们都可以在模式匹配时覆盖掉老的值,绑定新的:
```rust ```rust
fn main() { fn main() {
let age = Some(30); let age = Some(30);

@ -11,7 +11,7 @@
### 所有可能用到模式的地方 ### 所有可能用到模式的地方
#### match分支** #### match分支
```rust ```rust
match VALUE { match VALUE {
@ -32,7 +32,7 @@ match VALUE {
#### if let分支 #### if let分支
`if let`往往用于匹配一个模式,而忽略剩下的所有模式的场景: `if let`往往用于匹配一个模式,而忽略剩下的所有模式的场景:
```rust ```rust
if let Pattern = SOME_VALUE { if let PATTERN = SOME_VALUE {
} }
``` ```
@ -130,4 +130,4 @@ if let Some(x) = some_option_value {
} }
``` ```
因为`if let`允许匹配一种模式,而忽其余的模式。 因为`if let`允许匹配一种模式,而忽其余的模式。

@ -68,7 +68,7 @@ fn main() {
`impl Rectangle {}`表示为`Rectangle`实现方法(`impl` 是实现*implementation* 的缩写),这样的写法标明`impl`语句块中的一切都是跟`Rectangle`相关联的。 `impl Rectangle {}`表示为`Rectangle`实现方法(`impl` 是实现*implementation* 的缩写),这样的写法标明`impl`语句块中的一切都是跟`Rectangle`相关联的。
接下里的内容非常重要,请大家仔细看。在 `area` 的签名中,有一个我们之前没有看到过的关键字`&self`,该关键字指代的是`&Rectangle`类型,换句话说,`self`指代的是`Rectangle`结构体,这样的写法会让我们的代码简洁很多,而且非常便于理解: 我们为哪个结构体实现方法,那么`self`就是指代的该结构体自身 接下里的内容非常重要,请大家仔细看。在 `area` 的签名中,我们使用`&self`替代`rectangle: &Rectangle``&self`其实是`self: &Self`的简写(注意大小写)。在一个`impl`块内,`Self`指代被实现方法的结构体类型,`self`指代此类型的实例,换句话说,`self`指代的是`Rectangle`结构体实例,这样的写法会让我们的代码简洁很多,而且非常便于理解: 我们为哪个结构体实现方法,那么`self`就是指代的该结构体的实例
需要注意的是,`self`依然有所有权的概念: 需要注意的是,`self`依然有所有权的概念:
- `self`表示`Rectangle`的所有权转移到该方法中,这种形式用的较少 - `self`表示`Rectangle`的所有权转移到该方法中,这种形式用的较少
@ -129,7 +129,7 @@ fn main() {
} }
``` ```
用这种方式,我们可以把`Rectangle`的字段设置为私有属性,只需把它的`new`和`witdh`方法设置为公开可见,那么用户就可以创建一个矩形,同时通过访问器`rect1.width()`方法来获取矩形的宽度, 因为`width`字段是私有的,当用户访问`rect1.witdh`字段时,就会报错。 用这种方式,我们可以把`Rectangle`的字段设置为私有属性,只需把它的`new`和`width`方法设置为公开可见,那么用户就可以创建一个矩形,同时通过访问器`rect1.width()`方法来获取矩形的宽度, 因为`width`字段是私有的,当用户访问`rect1.width`字段时,就会报错。注意在此例中,`Self`指代的就是被实现方法的结构体`Rectangle`。
> ### `->` 运算符到哪去了? > ### `->` 运算符到哪去了?
> >
@ -261,4 +261,4 @@ m.call();
} }
``` ```
除了结构体和枚举,我们还能为特征(trait)实现方法,将在下章进行讲解,在此之前,先来看看泛型。 除了结构体和枚举,我们还能为特征(trait)实现方法,将在下章进行讲解,在此之前,先来看看泛型。

@ -52,7 +52,8 @@ trait Container<A,B> {
fn contains(&self,a: A,b: B) -> bool; fn contains(&self,a: A,b: B) -> bool;
} }
fn difference<A,B,C>(container: &C) -> i32 where fn difference<A,B,C>(container: &C) -> i32
where
C : Container<A,B> {...} C : Container<A,B> {...}
``` ```
@ -77,7 +78,7 @@ trait Add<RHS=Self> {
fn add(self, rhs: RHS) -> Self::Output; fn add(self, rhs: RHS) -> Self::Output;
} }
``` ```
它有一个泛型参数`RHS`,但是与我们以往的用法不同,这里它给`RHS`一个默认值,也就是当用户不指定`RHS`时,默认使用两个同样类型的值进行相加,然后返回一个关联类型`Outpu`。 它有一个泛型参数`RHS`,但是与我们以往的用法不同,这里它给`RHS`一个默认值,也就是当用户不指定`RHS`时,默认使用两个同样类型的值进行相加,然后返回一个关联类型`Output`。
可能上面那段不太好理解,下面我们用代码来举例: 可能上面那段不太好理解,下面我们用代码来举例:
```rust ```rust
@ -196,12 +197,9 @@ fn main() {
运行后依次输出: 运行后依次输出:
```console ```console
fn main() { This is your captain speaking.
let person = Human; Up!
Pilot::fly(&person); *waving arms furiously*
Wizard::fly(&person);
person.fly();
}
``` ```
因为`fly`方法的参数是`self`,当显示的调用时,编译器就可以根据调用的类型(`self`的类型)决定具体调用哪个方法。 因为`fly`方法的参数是`self`,当显示的调用时,编译器就可以根据调用的类型(`self`的类型)决定具体调用哪个方法。

@ -8,7 +8,7 @@ Go语言在2022年就要正式引入泛型被视为在1.0版本后,语
fn add_i8(a:i8, b:i8) -> i8 { fn add_i8(a:i8, b:i8) -> i8 {
a + b a + b
} }
fn add_i32(a:i16, b:i16) -> i16 { fn add_i32(a:i32, b:i32) -> i32 {
a + b a + b
} }
fn add_f64(a:f64, b:f64) -> f64 { fn add_f64(a:f64, b:f64) -> f64 {
@ -17,7 +17,7 @@ fn add_f64(a:f64, b:f64) -> f64 {
fn main() { fn main() {
println!("add i8: {}", add_i8(2i8, 3i8)); println!("add i8: {}", add_i8(2i8, 3i8));
println!("add i16: {}", add_i32(20, 30)); println!("add i32: {}", add_i32(20, 30));
println!("add f64: {}", add_f64(1.23, 1.23)); println!("add f64: {}", add_f64(1.23, 1.23));
} }
``` ```
@ -36,7 +36,7 @@ fn add<T>(a:T, b:T) -> T {
fn main() { fn main() {
println!("add i8: {}", add(2i8, 3i8)); println!("add i8: {}", add(2i8, 3i8));
println!("add i16: {}", add(20, 30)); println!("add i32: {}", add(20, 30));
println!("add f64: {}", add(1.23, 1.23)); println!("add f64: {}", add(1.23, 1.23));
} }
``` ```
@ -199,10 +199,10 @@ enum Result<T, E> {
该枚举和`Option`一样,主要用于函数返回值,与`Option`用于值的存在与否不同,`Result`关注的主要是正确性。 该枚举和`Option`一样,主要用于函数返回值,与`Option`用于值的存在与否不同,`Result`关注的主要是正确性。
如果函数正常运行,则最后返回一个`Ok(T)``T`是函数具体的返回值类型,如果函数异常运行,则返回一个`Err(E)``E`是具体的错误值。例如打开一个文件:当成功打开文件,返回`Ok(std::fs::File)`,因此`T` 对应的是 `std::fs::File` 类型;而当打开文件时出现问题时,返回`Err(std::io::Error)`, `E`对应的是`std::io::Error`类型。 如果函数正常运行,则最后返回一个`Ok(T)``T`是函数具体的返回值类型,如果函数异常运行,则返回一个`Err(E)``E`是错误类型。例如打开一个文件:当成功打开文件,返回`Ok(std::fs::File)`,因此`T` 对应的是 `std::fs::File` 类型;而当打开文件时出现问题时,返回`Err(std::io::Error)`, `E`对应的是`std::io::Error`类型。
## 方法中使用泛型 ## 方法中使用泛型
上一中,我们讲到何为方法以及如何在结构体和枚举上定义方法。方法上也可以使用泛型: 上一中,我们讲到何为方法以及如何在结构体和枚举上定义方法。方法上也可以使用泛型:
```rust ```rust
struct Point<T> { struct Point<T> {
x: T, x: T,
@ -268,7 +268,7 @@ impl Point<f32> {
## const泛型Rust最新版本引入的重要特性) ## const泛型Rust 1.51版本引入的重要特性)
在之前的泛型中,可以抽象为一句话:针对类型实现的泛型,所有的泛型都是为了抽象不同的类型,那有没有针对值的泛型?可能很多同学感觉很难理解,值怎么使用泛型?不急,我们先从数组讲起。 在之前的泛型中,可以抽象为一句话:针对类型实现的泛型,所有的泛型都是为了抽象不同的类型,那有没有针对值的泛型?可能很多同学感觉很难理解,值怎么使用泛型?不急,我们先从数组讲起。
在[数组](../compound-type/array.md)那节,有提到过很重要的一点:`[i32; 2]`和`[i32; 3]`是不同的数组类型,例如以下代码: 在[数组](../compound-type/array.md)那节,有提到过很重要的一点:`[i32; 2]`和`[i32; 3]`是不同的数组类型,例如以下代码:
@ -327,7 +327,7 @@ fn main() {
} }
``` ```
也不难唯一要注意的是需要对T加一个限制`std::fmt::Debug`,该限制表明`T`可以用在`println!("{:?}", arr)`中,因为`{"?}`形式的格式化输出需要`arr`实现该特征。 也不难唯一要注意的是需要对T加一个限制`std::fmt::Debug`,该限制表明`T`可以用在`println!("{:?}", arr)`中,因为`{:?}`形式的格式化输出需要`arr`实现该特征。
通过引用我们可以很轻松的解决处理任何类型数组的问题但是如果在某些场景下不适宜用引用或者干脆不能用呢那真的没什么好办法了你们知道为什么以前Rust的一些数组库在使用的时候都限定长度不超过32吗因为它们会为每个长度都单独实现一个函数简直。。。毫无人性。 通过引用我们可以很轻松的解决处理任何类型数组的问题但是如果在某些场景下不适宜用引用或者干脆不能用呢那真的没什么好办法了你们知道为什么以前Rust的一些数组库在使用的时候都限定长度不超过32吗因为它们会为每个长度都单独实现一个函数简直。。。毫无人性。
@ -356,7 +356,7 @@ fn main() {
## 泛型的性能 ## 泛型的性能
在Rust中泛型是零消耗的抽象,意味着你在使用泛型时,完全不用担心性能上的问题。 在Rust中泛型是零成本的抽象,意味着你在使用泛型时,完全不用担心性能上的问题。
但是任何选择都是权衡得失的既然我们获得了性能上的巨大优势那么又失去了什么呢Rust采用的是在编译期为泛型对应的多个类型生成各自的代码因此损失了编译速度和增大了最终生成文件的大小。 但是任何选择都是权衡得失的既然我们获得了性能上的巨大优势那么又失去了什么呢Rust采用的是在编译期为泛型对应的多个类型生成各自的代码因此损失了编译速度和增大了最终生成文件的大小。

@ -217,20 +217,38 @@ fn main() {
``` ```
因为`String`类型没有实现`Draw`特征,编译器直接就会报错,不会让上述代码运行。如果想要`String`类型被渲染在屏幕上,那么只需要为其实现`Draw`特征即可,非常容易。 因为`String`类型没有实现`Draw`特征,编译器直接就会报错,不会让上述代码运行。如果想要`String`类型被渲染在屏幕上,那么只需要为其实现`Draw`特征即可,非常容易。
#### &和dyn的区别 #### &dynBox\<dyn\>的区别
前文提到,`&`和`dyn`都可以用于特征对象,因此在功能上`&`和`dyn`几无区别,唯一的区别就是:`&`减少了一次指针调用。 前文提到,`&dyn`和`Box<dyn>`都可以用于特征对象,因此在功能上`&dyn`和`Box<dyn>`几无区别,唯一的区别就是:`&dyn`减少了一次指针调用。
因为`dyn`是一个宽指针(`fat pointer`), 它内部保存一个指针指向`vtable`,然后通过`vtable`查询到具体的函数指针,最后进行调用. 因为`Box<dyn>`是一个宽指针(`fat pointer`), 它内部保存一个指针指向`vtable`,然后通过`vtable`查询到具体的函数指针,最后进行调用.
所以,如果你在乎性能,又想使用特征对象简化代码,可以优先考虑`&`。 所以,如果你在乎性能,又想使用特征对象简化代码,可以优先考虑`&dyn`。
注意`dyn`不能单独作为特征对象的定义,例如下面的代码编译器会报错,原因是特征对象可以是任意实现了某个特征的类型,编译器在编译期不知道该类型的大小。
而`&dyn`和`Box<dyn>`在编译期都是已知大小,所以可以用作特征对象的定义。
```rust
fn draw2(x: dyn Draw) {
x.draw();
}
```
```
10 | fn draw2(x: dyn Draw) {
| ^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `(dyn Draw + 'static)`
help: function arguments must have a statically known size, borrowed types always have a known size
```
## 特征对象的动态分发 ## 特征对象的动态分发
回一下泛型章节我们提到过的,泛型是在编译期完成处理的:编译器会为每一个泛型参数对应的具体类型生成一份代码,这种方式是**静态分发(static dispatch)**,因为是在编译期完成的,对于运行期性能完全没有任何影响。 一下泛型章节我们提到过的,泛型是在编译期完成处理的:编译器会为每一个泛型参数对应的具体类型生成一份代码,这种方式是**静态分发(static dispatch)**,因为是在编译期完成的,对于运行期性能完全没有任何影响。
与静态分发相对应的是**动态分发(dynamic dispatch)**,在这种情况下,直到运行时,才能确定需要调用什么方法。 与静态分发相对应的是**动态分发(dynamic dispatch)**,在这种情况下,直到运行时,才能确定需要调用什么方法。
当使用特赠对象时Rust 必须使用动态分发。编译器无法知晓所有可能用于特征对象代码的类型所以它也不知道应该调用哪个类型的哪个方法实现。为此Rust 在运行时使用特征对象中的指针来知晓需要调用哪个方法。动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。 当使用特对象时Rust 必须使用动态分发。编译器无法知晓所有可能用于特征对象代码的类型所以它也不知道应该调用哪个类型的哪个方法实现。为此Rust 在运行时使用特征对象中的指针来知晓需要调用哪个方法。动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。
## Self与self ## Self与self
在Rust中有两个`self`,一个指代当前的实例对象,一个指代特征或者方法类型的别名: 在Rust中有两个`self`,一个指代当前的实例对象,一个指代特征或者方法类型的别名:
@ -264,7 +282,7 @@ fn main() {
对象安全对于特征对象是必须的,因为一旦有了特征对象,就不再知道实现该特征的具体类型是什么了。如果特征方法返回具体的`Self`类型,但是特征对象忘记了其真正的类型,那这个`Self`就非常尴尬,因为没人知道它是谁了。同理对于泛型类型参数来说,当使用特征时其会放入具体的类型参数:此具体类型变成了实现该特征的类型的一部分。当使用特征对象时其具体类型被抹去了,故而无从得知放入泛型参数类型到底是什么。 对象安全对于特征对象是必须的,因为一旦有了特征对象,就不再知道实现该特征的具体类型是什么了。如果特征方法返回具体的`Self`类型,但是特征对象忘记了其真正的类型,那这个`Self`就非常尴尬,因为没人知道它是谁了。同理对于泛型类型参数来说,当使用特征时其会放入具体的类型参数:此具体类型变成了实现该特征的类型的一部分。当使用特征对象时其具体类型被抹去了,故而无从得知放入泛型参数类型到底是什么。
标准库中的 `Clone`特征就符合对象安全的要求: 标准库中的 `Clone`特征就符合对象安全的要求:
```rust ```rust
pub trait Clone { pub trait Clone {

@ -123,7 +123,7 @@ impl Summary for Weibo {
sunface发表了微博好像微博没Tweet好用 sunface发表了微博好像微博没Tweet好用
``` ```
默认实现允许调用相同特中的其他方法,哪怕这些方法没有默认实现。如此,特征可以提供很多有用的功能而只需要实现指定的一小部分内容。例如,我们可以定义`Summary`特征,使其具有一个需要实现的`summarize_author`方法,然后定义一个`summarize`方法,此方法的默认实现调用`summarize_author`方法: 默认实现允许调用相同特中的其他方法,哪怕这些方法没有默认实现。如此,特征可以提供很多有用的功能而只需要实现指定的一小部分内容。例如,我们可以定义`Summary`特征,使其具有一个需要实现的`summarize_author`方法,然后定义一个`summarize`方法,此方法的默认实现调用`summarize_author`方法:
```rust ```rust
pub trait Summary { pub trait Summary {
fn summarize_author(&self) -> String; fn summarize_author(&self) -> String;
@ -157,7 +157,7 @@ pub fn notify(item: &impl Summary) {
} }
``` ```
`impl Summary`,只能说出这个类型的人真的是起名鬼才,简直太贴切了,故名思义`实现了Summary特征`的`item参数. `impl Summary`,只能说出这个类型的人真的是起名鬼才,简直太贴切了,故名思义`实现了Summary特征`的`item`参数.
你可以使用任何实现了`Summary`特征的类型作为该函数的参数,同时在函数体内,还可以调用该特征的的方法,例如`summarize`方法。具体的说,可以传递`Post`或`Weibo`的实例来作为参数,而其它类如`String`或者`i32`的类型则不能用做该函数的参数,因为它们没有实现`Summary`特征。 你可以使用任何实现了`Summary`特征的类型作为该函数的参数,同时在函数体内,还可以调用该特征的的方法,例如`summarize`方法。具体的说,可以传递`Post`或`Weibo`的实例来作为参数,而其它类如`String`或者`i32`的类型则不能用做该函数的参数,因为它们没有实现`Summary`特征。
@ -237,8 +237,8 @@ impl<T: Display + PartialOrd> Pair<T> {
} }
``` ```
`cmd_display`方法,并不是所有的`Pair<T>`结构体对象都拥有,只有`T`实现了`Display + PartialOrd`的`Part<T>`才拥有此方法 `cmd_display`方法,并不是所有的`Pair<T>`结构体对象都拥有,只有`T`实现了`Display + PartialOrd`的`Part<T>`才拥有此方法
该函数可读性会更好,因为泛型参数、参数、返回值都在一起,可以快速的阅读,同时每个泛型参数的特征也在新的代码行中通过`where`进行了约束。 该函数可读性会更好,因为泛型参数、参数、返回值都在一起,可以快速的阅读,同时每个泛型参数的特征也在新的代码行中通过**特征约束**进行了约束。
**也可以有条件的实现特征**, 例如,标准库为任何实现了 `Display`特征的类型实现了 `ToString`特征: **也可以有条件的实现特征**, 例如,标准库为任何实现了 `Display`特征的类型实现了 `ToString`特征:
```rust ```rust
@ -390,7 +390,7 @@ fn main() {
例如`Copy`特征,它也有一套自动实现的默认代码,当标记到一个类型上时,可以让这个类型自动实现`Copy`特征,进而可以调用`copy`方法,进行自我复制。 例如`Copy`特征,它也有一套自动实现的默认代码,当标记到一个类型上时,可以让这个类型自动实现`Copy`特征,进而可以调用`copy`方法,进行自我复制。
`derive`派生出来的是Rust默认给我们提供的特征在开发过程中极大的简化了自己手动实现相应特征的需求当然如果你有特殊的需求还可以自己手动重载该实现。 `derive`派生出来的是Rust默认给我们提供的特征在开发过程中极大的简化了自己手动实现相应特征的需求当然如果你有特殊的需求还可以自己手动重载该实现。
详细的`derive`列表参加[附录-派生特征](../../appendix/derive.md). 详细的`derive`列表参加[附录-派生特征](../../appendix/derive.md).

Loading…
Cancel
Save