Merge pull request #446 from 1132719438/main

Some fixes in cargo, fight-with-compiler and pitfalls chapter
pull/448/head
孙飞Sunface 3 years ago committed by GitHub
commit 9969ed10f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,5 +1,5 @@
# 构建( Build )缓存
`cargo build` 的结果会被放入项目根目录下的 `target` 文件夹中,当然,这个位置可以三种方式更改:设置 `CARGO_TARGET_DIR` [环境变量](https://course.rs/cargo/reference/env.html)、[`build.target-dir`]((https://course.rs/cargo/reference/configuration.html#buildtarget-dir)) 配置项以及 `--target-dir` 命令行参数。
`cargo build` 的结果会被放入项目根目录下的 `target` 文件夹中,当然,这个位置可以三种方式更改:设置 `CARGO_TARGET_DIR` [环境变量](https://course.rs/cargo/reference/env.html)、[`build.target-dir`](https://course.rs/cargo/reference/configuration.html#buildtarget-dir) 配置项以及 `--target-dir` 命令行参数。
## target 目录结构
`target` 目录的结构取决于是否使用 `--target` 标志为特定的平台构建。

@ -42,11 +42,11 @@ $ echo $HOME/.cargo/
- `registry/cache/`
- `git/db/`
## 清缓存
## 清缓存
理论上,我们可以手动移除缓存中的任何一部分,当后续有包需要时 `Cargo` 会尽可能去恢复这些资源:
- 解压缩 `registry/src` 下的 `.crate` 档案
- 解压缩 `registry/cache` 下的 `.crate` 档案
- 从 `.git``checkout` 缓存的仓库
- 如果以上都没了,会从网络上重新下载
你也可以使用 [cargo-cache] 包来选择性的清除 `cache` 中指定的部分,当然,它还可以用来查看缓存中的组件大小。
你也可以使用 [cargo-cache](https://crates.io/crates/cargo-cache) 包来选择性的清除 `cache` 中指定的部分,当然,它还可以用来查看缓存中的组件大小。

@ -18,7 +18,7 @@
**那么问题来了,为何会有这种选择?**
原因是 `Cargo.lock`尽描述上一次成功构建的各种信息环境状态、依赖、版本等等Cargo 可以使用它提供确定性的构建环境和流程,无论何时何地。这种特性对于终端服务是非常重要的:能确定、稳定的在用户环境中运行起来是终端服务最重要的特性之一。
原因是 `Cargo.lock`尽描述上一次成功构建的各种信息环境状态、依赖、版本等等Cargo 可以使用它提供确定性的构建环境和流程,无论何时何地。这种特性对于终端服务是非常重要的:能确定、稳定的在用户环境中运行起来是终端服务最重要的特性之一。
而对于三方库来说,情况就有些不同。它不仅仅被库的开发者所使用,还会间接影响依赖链下游的使用者。用户引入了三方库是不会去看它的 `Cargo.lock` 信息的,也不应该受这个库的确定性运行条件所限制。
@ -35,7 +35,7 @@ version = "0.1.0"
regex = { git = "https://github.com/rust-lang/regex.git" }
```
可以看到,只有一个依赖,且该依赖的来源是 `Github` 上一个特定的仓库。由于我们没有指定任何版本信息,`Cargo` 会自动拉取该依赖库的最新版本( `master` 分支上的最新 `commit` )。
可以看到,只有一个依赖,且该依赖的来源是 `Github` 上一个特定的仓库。由于我们没有指定任何版本信息,`Cargo` 会自动拉取该依赖库的最新版本( `master` `main` 分支上的最新 `commit` )。
这种使用方式,其实就错失了包管理工具的最大的优点:版本管理。例如你在今天构建使用了版本 `A`,然后过了一段时间后,由于依赖包的升级,新的构建却使用了大更新版本 `B`,结果因为版本不兼容,导致了构建失败。

@ -1,7 +1,7 @@
# 智能指针引起的重复借用错误
本文将彻底解决一个困扰广大Rust用户已久的常见错误 当智能指针和结构体一起使用时导致的借用错误: ` cannot borrow `mut_s` as mutable because it is also borrowed as immutable`.
本文将彻底解决一个困扰广大 Rust 用户已久的常见错误: 当智能指针和结构体一起使用时导致的借用错误: ` cannot borrow `mut_s` as mutable because it is also borrowed as immutable`.
相信看过[<<对抗Rust编译检查系列>>](https://www.zhihu.com/column/c_1461712984854335488)的读者都知道结构体中的不同字段可以独立借用吧?
相信看过[<<对抗Rust编译检查系列>>](https://course.rs/fight-with-compiler/intro.html)的读者都知道结构体中的不同字段可以独立借用吧?
## 结构体中的字段借用
不知道也没关系,我们这里再简单回顾一下:
@ -21,12 +21,12 @@ impl Test {
}
```
这段代码看上去像是重复借用了`&mut self`,违反了Rust的借用规则实际上在聪明的Rust编译器面前这都不是事。它能发现我们其实借用了目标结构体的不同字段因此完全可以将其借用权分离开来。
这段代码看上去像是重复借用了`&mut self`,违反了 Rust 的借用规则,实际上在聪明的 Rust 编译器面前,这都不是事。它能发现我们其实借用了目标结构体的不同字段,因此完全可以将其借用权分离开来。
因此,虽然我们不能同时对整个结构体进行多次可变借用,但是我们可以分别对结构体中的不同字段进行可变借用,当然,一个字段至多也只能存在一个可变借用,这个最基本的所有权规则还是不能违反的。变量`a`引用结构体字段`a`,变量`b`引用结构体字段`b`,从底层来说,这种方式也不会造成两个可变引用指向了同一块内存。
## RefCell
如果你还不知道RefCell可以看看[这篇文章](https://zhuanlan.zhihu.com/p/453727091)当然不看也行简而言之RefCell能够实现:
如果你还不知道 RefCell可以看看[这篇文章](https://course.rs/advance/smart-pointer/cell-refcell.html)当然不看也行简而言之RefCell 能够实现:
- 将借用规则从编译期推迟到运行期,但是并不会饶过借用规则,当不符合时,程序直接`panic`
- 实现内部可变性:简单来说,对一个不可变的值进行可变借用,然后修改内部的值
@ -86,7 +86,7 @@ fn write(s: RefCell<S>) {
}
```
可以看出,对结构体字段的调用,实际上经过一层函数,一层函数!?我相信你应该想起了什么,是的,在[上一篇文章](https://zhuanlan.zhihu.com/p/451920390/edit)中讲过类似的问题, 大意就是**编译器对于函数往往只会分析签名,并不关心内部到底如何使用结构体**。
可以看出,对结构体字段的调用,实际上经过一层函数,一层函数!?我相信你应该想起了什么,是的,在[上一篇文章](https://course.rs/fight-with-compiler/borrowing/ref-exist-in-out-fn.html)中讲过类似的问题, 大意就是**编译器对于函数往往只会分析签名,并不关心内部到底如何使用结构体**。
而上面的`&Deref::deref(&mut_s)`和`DerefMut::deref_mut(&mut mut_s)`函数,签名全部使用的是结构体,并不是结构体中的某一个字段,因此对于编译器来说,该结构体明显是被重复借用了!
@ -125,7 +125,7 @@ fn write(s: RefCell<S>) {
可以看出,此时对结构体的使用不再有`DerefMut::deref`的身影,我们成功消除了函数边界对编译器的影响!
## 不仅仅是RefCell
事实上除了RefCell外还有不少会导致这种问题的智能指针当然原理都是互通的我们这里就不再进行一一深入讲解只简单列举下
事实上,除了 RefCell 外,还有不少会导致这种问题的智能指针,当然原理都是互通的,我们这里就不再进行一一深入讲解,只简单列举下:
- `Box`
- `MutexGuard`(来源于Mutex)

@ -1,8 +1,8 @@
# 同时在函数内外使用引用导致的重复借用错误
本文将彻底解决一个困扰广大Rust用户已久的常见错误因为在函数内外同时借用一个引用导致了重复借用错误`cannot borrow *self as mutable because it is also borrowed as immutable`.
本文将彻底解决一个困扰广大 Rust 用户已久的常见错误:因为在函数内外同时借用一个引用,导致了重复借用错误`cannot borrow *self as mutable because it is also borrowed as immutable`.
> 本文大部分内容节选自[Rust陷阱系列](https://www.zhihu.com/column/c_1454754106916806656)专题,由于借用是新手绕不过去的坎,因此将其提取出来形成一个新的系列
> 本文大部分内容节选自[Rust常见陷阱](https://course.rs/pitfalls/index.html)专题,由于借用是新手绕不过去的坎,因此将其提取出来形成一个新的系列
## 正确的代码
```rust
@ -21,12 +21,12 @@ impl Test {
}
```
这段代码是可以正常编译的,也许有读者会有疑问,`self`在这里被两个变量以可变的方式借用了明明违反了Rust的所有权规则为何它不会报错
这段代码是可以正常编译的,也许有读者会有疑问,`self`在这里被两个变量以可变的方式借用了,明明违反了 Rust 的所有权规则,为何它不会报错?
答案要从很久很久之前开始(啊哒~~~由于我太啰嗦,被正义群众来了一下,那咱现在开始长话短说,直接进入主题)。
#### 正确代码为何不报错?
虽然从表面来看,`a`和`b`都可变引用了`self`但是Rust的编译器在很多时候都足够聪明它发现我们其实仅仅引用了同一个结构体中的不同字段因此完全可以将其的借用权分离开来。
虽然从表面来看,`a`和`b`都可变引用了`self`,但是 Rust 的编译器在很多时候都足够聪明,它发现我们其实仅仅引用了同一个结构体中的不同字段,因此完全可以将其的借用权分离开来。
因此,虽然我们不能同时对整个结构体进行可变引用,但是我们可以分别对结构体中的不同字段进行可变引用,当然,一个字段至多也只能存在一个可变引用,这个最基本的所有权规则还是不能违反的。变量`a`引用结构体字段`a`,变量`b`引用结构体字段`b`,从底层来说,这种方式也不会造成两个可变引用指向了同一块内存。
@ -72,7 +72,7 @@ error[E0499]: cannot borrow `*self` as mutable more than once at a time
## 大聪明编译器
为什么?明明之前还是正确的代码,就因为放入函数中就报错了?我们先从一个简单的理解谈起,当然这个理解也是浮于表面的,等会会深入分析真实的原因。
之前讲到Rust编译器挺聪明可以识别到引用到不同的结构体字段因此不会报错。但是现在这种情况下编译器又不够聪明了一旦放入函数中编译器将无法理解我们对`self`的使用:它仅仅用到了一个字段,而不是整个结构体。
之前讲到 Rust 编译器挺聪明,可以识别到引用到不同的结构体字段,因此不会报错。但是现在这种情况下,编译器又不够聪明了,一旦放入函数中,编译器将无法理解我们对`self`的使用:它仅仅用到了一个字段,而不是整个结构体。
因此它会简单的认为,这个结构体作为一个整体被可变借用了,产生两个可变引用,一个引用整个结构体,一个引用了结构体字段`b`,这两个引用存在重叠的部分,最终导致编译错误。
@ -103,13 +103,13 @@ fn increase(&mut self) {
为何会有这种编译器行为,主要有两个原因:
1. 一般来说,我们希望编译器有能力独立的编译每个函数,而无需深入到相关函数的内部实现,因为这样做会带来快得多的编译速度。
2. 如果没有这种保证,那么在实际项目开发中,我们会特别容易遇到各种错误。 假设我们要求编译器不仅仅关注相关函数的签名还要深入其内部关注实现那么由于Rust严苛的编译规则当你修改了某个函数内部实现的代码后可能会引起使用该函数的其它函数的各种错误对于大型项目来说这几乎是不可接受的
2. 如果没有这种保证,那么在实际项目开发中,我们会特别容易遇到各种错误。 假设我们要求编译器不仅仅关注相关函数的签名,还要深入其内部关注实现,那么由于 Rust 严苛的编译规则,当你修改了某个函数内部实现的代码后,可能会引起使用该函数的其它函数的各种错误!对于大型项目来说,这几乎是不可接受的!
然后我们的借用类型这么简单编译器有没有可能针对这种场景在现有的借用规则之外增加特殊规则答案是否定的由于Rust语言的设计哲学特殊规则的加入需要慎之又慎而我们的这种情况其实还蛮好解决的因此编译器不会为此新增规则。
然后,我们的借用类型这么简单,编译器有没有可能针对这种场景,在现有的借用规则之外增加特殊规则?答案是否定的,由于 Rust 语言的设计哲学:特殊规则的加入需要慎之又慎,而我们的这种情况其实还蛮好解决的,因此编译器不会为此新增规则。
## 解决办法
在深入分析中,我们提到一条重要的规则,要影响编译行为,就需要更改相关函数的签名,因此可以修改`increate_a`的签名:
在深入分析中,我们提到一条重要的规则,要影响编译行为,就需要更改相关函数的签名,因此可以修改`increase_a`的签名:
```rust
fn increase_a (a :&mut u32) {
*a += 1;

@ -2,7 +2,7 @@
特征对象是一个好东西闭包也是一个好东西但是如果两者你都想要时可能就会火星撞地球boom! 至于这两者为何会勾搭到一起?考虑一个常用场景:使用闭包作为回调函数.
## 学习目标
如何使用闭包作为特征对象,并解决以下错误:`the parameter type `impl Fn(&str) -> Res` may not live long enough`
如何使用闭包作为特征对象,并解决以下错误:`the parameter type ` \`impl Fn(&str) -> Res\` ` may not live long enough`
## 报错的代码
在下面代码中,我们通过闭包实现了一个简单的回调函数(错误代码已经标注)
@ -49,6 +49,16 @@ fn main() {
}
```
```
error[E0310]: the parameter type `impl Fn(&str) -> Res` may not live long enough
--> src/main.rs:25:30
|
24 | pub fn set(&mut self, cb: impl Fn(&str) -> Res) {
| -------------------- help: consider adding an explicit lifetime bound...: `impl Fn(&str) -> Res + 'static`
25 | self.callback = Some(Box::new(cb));
| ^^^^^^^^^^^^ ...so that the type `impl Fn(&str) -> Res` will meet its required lifetime bounds
```
从第一感觉来说,报错属实不应该,因为我们连引用都没有用,生命周期都不涉及,怎么就报错了?在继续深入之前,先来观察下该闭包是如何被使用的:
```rust
callback: Option<Box<dyn Fn(&str) -> Res>>,
@ -121,7 +131,7 @@ inl.set(move |val| {
// println!("{}", local);
```
如上所示,你也可以选择将本地变量的所有权`move`进闭包中,此时闭包再次具有`'statci`生命周期
如上所示,你也可以选择将本地变量的所有权`move`进闭包中,此时闭包再次具有`'static`生命周期
##### 4. 非要捕获本地变量的引用?
对于第2种情况如果非要这么干那`'static`肯定是没办法了,我们只能给予闭包一个新的生命周期:

@ -1,13 +1,13 @@
# 算术溢出导致的panic
在Rust中溢出后的数值被截断是很正常的:
Rust 中,溢出后的数值被截断是很正常的:
```rust
let x: u16 = 65535;
let v = x as u8;
println!("{}", v)
```
最终程序会输出`255`, 因此大家可能会下意识地就觉得算数操作在Rust中只会导致结果的不正确并不会导致异常。但是实际上如果是因为算术操作符导致的溢出就会让整个程序panic:
最终程序会输出`255`, 因此大家可能会下意识地就觉得算数操作在 Rust 中只会导致结果的不正确,并不会导致异常。但是实际上,如果是因为算术操作符导致的溢出,就会让整个程序 panic:
```rust
fn main() {
let x: u8 = 10;
@ -22,7 +22,7 @@ fn main() {
thread 'main' panicked at 'attempt to add with overflow', src/main.rs:5:13
```
那么当我们确实有这种需求时该如何做呢可以使用Rust提供的`checked_xxx`系列方法:
那么当我们确实有这种需求时,该如何做呢?可以使用 Rust 提供的`checked_xxx`系列方法:
```rust
fn main() {
let x: u8 = 10;
@ -54,9 +54,9 @@ pub fn working_items_per_minute(speed: u8) -> u32 {
}
```
上述代码中,`speed * cph`就会直接panic:
上述代码中,`speed * cph`就会直接 panic:
```console
thread 'main' panicked at 'attempt to multiply with overflow', src/main.rs:10:18
```
是不是还藏的挺隐蔽的因此大家在Rust中做数学运算时要多留一个心眼免得上了生产才发现问题所在。或者你也可以做好单元测试:)
是不是还藏的挺隐蔽的?因此大家在 Rust 中做数学运算时,要多留一个心眼,免得上了生产才发现问题所在。或者,你也可以做好单元测试:)

@ -1,6 +1,6 @@
# 闭包上奇怪的生命周期
Rust一道独特的靓丽风景就是生命周期也是反复折磨新手的最大黑手就连老手可能一不注意就会遇到一些生命周期上的陷阱例如闭包上使用引用。
Rust 一道独特的靓丽风景就是生命周期,也是反复折磨新手的最大黑手,就连老手,可能一不注意就会遇到一些生命周期上的陷阱,例如闭包上使用引用。
## 一段简单的代码
先来看一段简单的代码:
@ -23,7 +23,7 @@ error: lifetime may not live long enough
咦?竟然报错了,明明两个一模一样功能的函数,一个正常编译,一个却报错,错误原因是编译器无法推测返回的引用和传入的引用谁活得更久!
真的是非常奇怪的错误,学过[Rust生命周期](https://github.com/sunface/rust-course/blob/main/src/advance/lifetime/basic.md)的读者应该都记得这样一条生命周期消除规则: **如果函数参数中只有一个引用类型,那该引用的生命周期会被自动分配给所有的返回引用**。我们当前的情况完美符合,`function`函数的顺利编译通过,就充分说明了问题。
真的是非常奇怪的错误,学过[Rust生命周期](https://course.rs/advance/lifetime/basic.html)的读者应该都记得这样一条生命周期消除规则: **如果函数参数中只有一个引用类型,那该引用的生命周期会被自动分配给所有的返回引用**。我们当前的情况完美符合,`fn_elision`函数的顺利编译通过,就充分说明了问题。
那为何闭包就出问题了?
@ -100,9 +100,9 @@ let closure_slision = |x: &i32| -> &i32 { x };
编译器就必须深入到闭包函数体中,去分析和推测生命周期,复杂度因此极具提升:试想一下,编译器该如何从复杂的上下文中分析出参数引用的生命周期和闭包体中生命周期的关系?
由于上述原因(当然,实际情况复杂的多)Rust语言开发者其实目前是有意为之针对函数和闭包实现了两种不同的生命周期消除规则。
由于上述原因(当然,实际情况复杂的多) Rust 语言开发者其实目前是有意为之,针对函数和闭包实现了两种不同的生命周期消除规则。
## 总结
虽然我言之凿凿,闭包的生命周期无法解决,但是未来谁又知道呢。最大的可能性就是之前开头那种简单的场景,可以被自动识别和消除。
总之,如果有这种需求,还是像古天乐一样做一个平平无奇的男人,老老实实使用函数吧.
总之,如果有这种需求,还是像古天乐一样做一个平平无奇的男人,老老实实使用函数吧

@ -1,3 +1,3 @@
# Rust陷阱系列
本章收录一些Rust常见的陷阱一不小心就会坑你的那种(当然这不是Rust语言的问题而是一些边边角角知识点)。
本章收录一些 Rust 常见的陷阱,一不小心就会坑你的那种(当然,这不是 Rust 语言的问题,而是一些边边角角知识点)。

@ -1,5 +1,5 @@
# 无处不在的迭代器
Rust的迭代器无处不在直至你在它上面栽了跟头经过深入调查才发现原来是迭代器的锅。不信的话看看这个报错你能想到是迭代器的问题吗: `borrow of moved value: words`.
Rust 的迭代器无处不在,直至你在它上面栽了跟头,经过深入调查才发现:哦,原来是迭代器的锅。不信的话,看看这个报错你能想到是迭代器的问题吗: `borrow of moved value: words`.
## 报错的代码
以下的代码非常简单,用来统计文本中字词的数量,并打印出来:
@ -71,7 +71,7 @@ let n = words.clone().count();
在继续之前,我得先找一个地方藏好,因为俺有一个感觉,烂西红柿正在铺天盖地的呼啸而来,伴随而来的是读者的正义呵斥:
**你管`clone`叫最好、最`rusty`的解决方法??**
大家且听我慢慢道来事实上在Rust中`clone`不总是性能低下的代名词,因为`clone`的行为完全取决于它的具体实现。
大家且听我慢慢道来,事实上,在 Rust 中`clone`不总是性能低下的代名词,因为`clone`的行为完全取决于它的具体实现。
#### 迭代器的`clone`代价
对于迭代器而言,它其实并不需要持有数据才能进行迭代,事实上它包含一个引用,该引用指向了保存在堆上的数据,而迭代器自身的结构是保存在栈上。
@ -102,6 +102,6 @@ where
以上代码实现了对`Split`迭代器的克隆,可以看出,底层的的数组`self.v`并没有被克隆而是简单的复制了一个引用,依然指向了底层的数组`&[T]`,因此这个克隆非常高效。
## 总结
看起来是无效借用导致的错误实际上是迭代器被消费了导致的问题这说明Rust编译器虽然会告诉你错误原因但是这个原因不总是根本原因。我们需要一双慧眼和勤劳的手来挖掘出这个宝藏最后为己所用。
看起来是无效借用导致的错误,实际上是迭代器被消费了导致的问题,这说明 Rust 编译器虽然会告诉你错误原因,但是这个原因不总是根本原因。我们需要一双慧眼和勤劳的手,来挖掘出这个宝藏,最后为己所用。
同时克隆在Rust中也并不总是**bad guy**的代名词,有的时候我们可以大胆去使用,当然前提是了解你的代码场景和具体的`clone`实现,这样你也能像文中那样作出非常`rusty`的选择。
同时,克隆在 Rust 中也并不总是**bad guy**的代名词,有的时候我们可以大胆去使用,当然前提是了解你的代码场景和具体的`clone`实现,这样你也能像文中那样作出非常`rusty`的选择。

@ -1,8 +1,8 @@
# 不太勤快的迭代器
迭代器在Rust中是一个非常耀眼的存在它光鲜亮丽它让Rust大道至简它备受用户的喜爱。可是它也是懒惰的不信一起来看看。
迭代器,在 Rust 中是一个非常耀眼的存在,它光鲜亮丽,它让 Rust 大道至简,它备受用户的喜爱。可是,它也是懒惰的,不信?一起来看看。
## for循环 vs 迭代器
在迭代器学习中,我们提到过迭代器在功能上可以替代循环,性能上略微优于循环(避免边界检查),安全性上优于循环因此在Rust中迭代器往往都是更优的选择前提是迭代器得发挥作用。
在迭代器学习中,我们提到过迭代器在功能上可以替代循环,性能上略微优于循环(避免边界检查),安全性上优于循环,因此在 Rust 中,迭代器往往都是更优的选择,前提是迭代器得发挥作用。
在下面代码中,分别是使用`for`循环和迭代器去生成一个`HashMap`。
@ -71,7 +71,7 @@ accounts.into_iter().map(|a| {
> 事实上IDE和编译器都会对这种代码给出警告iterators are lazy and do nothing unless consumed
## 解决办法
原因非常清晰,如果读者还有疑惑,建议深度下上面给出的迭代器链接,我们这里就不再赘述。
原因非常清晰,如果读者还有疑惑,建议深度了解下上面给出的迭代器链接,我们这里就不再赘述。
下面列出三种合理的解决办法:

@ -1,7 +1,7 @@
# 线程间传递消息导致主线程无法结束
本篇陷阱较短,主要解决新手在多线程间传递消息时可能会遇到的一个问题:主线程会一直阻塞,无法结束。
Rust标准库中提供了一个消息通道非常好用也相当简单明了但是但是在使用起来还是可能存在坑
Rust 标准库中提供了一个消息通道,非常好用,也相当简单明了,但是但是在使用起来还是可能存在坑:
```rust
use std::sync::mpsc;
fn main() {
@ -39,7 +39,7 @@ Got: 2
其实,上面的描述有问题,主线程并不是卡死,而是`for`循环并没有结束,至于`for`循环不结束的原因是消息通道没有被关闭。
回忆一下Rust消息通道关闭的两个条件所有发送者全部被`drop`或接收者被`drop`,由于`for`循环还在使用接收者,因为后者条件无法被满足,那么只能发送者全部被`drop`,才能让例子中的消息通道关闭。
回忆一下 Rust 消息通道关闭的两个条件:所有发送者全部被`drop`或接收者被`drop`,由于`for`循环还在使用接收者,因为后者条件无法被满足,那么只能发送者全部被`drop`,才能让例子中的消息通道关闭。
来分析下代码,每一个子线程都从`send`获取了一个拷贝,然后该拷贝在子线程结束时自动被`drop`,看上去没问题啊。等等,好像`send`本身并没有被`drop`,因为`send`要等到`main`函数结束才会被`drop`,那么代码就陷入了一个尴尬的境地:`main`函数要结束需要`for`循环结束,`for`循环结束需要`send`被`drop`,而`send`要被`drop`需要`main`函数结束。。。

@ -1,6 +1,6 @@
# 代码重构导致的可变借用错误
相信大家都听说过**重构一时爽,一直重构一直爽**的说法私以为这种说法是很有道理的不然技术团队绩效从何而来但是在Rust中重构可能就不是那么爽快的事了不信咱们来看看。
相信大家都听说过**重构一时爽,一直重构一直爽**的说法,私以为这种说法是很有道理的,不然技术团队绩效从何而来?但是,在 Rust 中,重构可能就不是那么爽快的事了,不信?咱们来看看。
## 欣赏下报错
很多时候,错误也是一种美,但是当这种错误每天都能见到时(呕):
@ -27,12 +27,12 @@ impl Test {
}
```
这段代码是可以正常编译的,也许有读者会有疑问,`self`在这里被两个变量以可变的方式借用了明明违反了Rust的所有权规则为何它不会报错
这段代码是可以正常编译的,也许有读者会有疑问,`self`在这里被两个变量以可变的方式借用了,明明违反了 Rust 的所有权规则,为何它不会报错?
答案要从很久很久之前开始(啊哒~~~由于我太啰嗦,被正义群众来了一下,那咱现在开始长话短说,直接进入主题)。
#### 正确代码为何不报错?
虽然从表面来看,`a`和`b`都可变引用了`self`但是Rust的编译器在很多时候都足够聪明它发现我们其实仅仅引用了同一个结构体中的不同字段因此完全可以将其的借用权分离开来。
虽然从表面来看,`a`和`b`都可变引用了`self`,但是 Rust 的编译器在很多时候都足够聪明,它发现我们其实仅仅引用了同一个结构体中的不同字段,因此完全可以将其的借用权分离开来。
因此,虽然我们不能同时对整个结构体进行可变引用,但是我们可以分别对结构体中的不同字段进行可变引用,当然,一个字段至多也只能存在一个可变引用,这个最基本的所有权规则还是不能违反的。变量`a`引用结构体字段`a`,变量`b`引用结构体字段`b`,从底层来说,这种方式也不会造成两个可变引用指向了同一块内存。
@ -80,7 +80,7 @@ error[E0499]: cannot borrow `*self` as mutable more than once at a time
## 大聪明编译器
为什么?明明之前还是正确的代码,就因为放入函数中就报错了?我们先从一个简单的理解谈起,当然这个理解也是浮于表面的,等会会深入分析真实的原因。
之前讲到Rust编译器挺聪明可以识别到引用到不同的结构体字段因此不会报错。但是现在这种情况下编译器又不够聪明了一旦放入函数中编译器将无法理解我们对`self`的使用:它仅仅用到了一个字段,而不是整个结构体。
之前讲到 Rust 编译器挺聪明,可以识别到引用到不同的结构体字段,因此不会报错。但是现在这种情况下,编译器又不够聪明了,一旦放入函数中,编译器将无法理解我们对`self`的使用:它仅仅用到了一个字段,而不是整个结构体。
因此它会简单的认为,这个结构体作为一个整体被可变借用了,产生两个可变引用,一个引用整个结构体,一个引用了结构体字段`b`,这两个引用存在重叠的部分,最终导致编译错误。
@ -111,13 +111,13 @@ fn increase(&mut self) {
为何会有这种编译器行为,主要有两个原因:
1. 一般来说,我们希望编译器有能力独立的编译每个函数,而无需深入到相关函数的内部实现,因为这样做会带来快得多的编译速度。
2. 如果没有这种保证,那么在实际项目开发中,我们会特别容易遇到各种错误。 假设我们要求编译器不仅仅关注相关函数的签名还要深入其内部关注实现那么由于Rust严苛的编译规则当你修改了某个函数内部实现的代码后可能会引起使用该函数的其它函数的各种错误对于大型项目来说这几乎是不可接受的
2. 如果没有这种保证,那么在实际项目开发中,我们会特别容易遇到各种错误。 假设我们要求编译器不仅仅关注相关函数的签名,还要深入其内部关注实现,那么由于 Rust 严苛的编译规则,当你修改了某个函数内部实现的代码后,可能会引起使用该函数的其它函数的各种错误!对于大型项目来说,这几乎是不可接受的!
然后我们的借用类型这么简单编译器有没有可能针对这种场景在现有的借用规则之外增加特殊规则答案是否定的由于Rust语言的设计哲学特殊规则的加入需要慎之又慎而我们的这种情况其实还蛮好解决的因此编译器不会为此新增规则。
然后,我们的借用类型这么简单,编译器有没有可能针对这种场景,在现有的借用规则之外增加特殊规则?答案是否定的,由于 Rust 语言的设计哲学:特殊规则的加入需要慎之又慎,而我们的这种情况其实还蛮好解决的,因此编译器不会为此新增规则。
## 解决办法
在深入分析中,我们提到一条重要的规则,要影响编译行为,就需要更改相关函数的签名,因此可以修改`increate_a`的签名:
在深入分析中,我们提到一条重要的规则,要影响编译行为,就需要更改相关函数的签名,因此可以修改`increase_a`的签名:
```rust
fn increase_a (a :&mut u32) {
*a += 1;

@ -1,6 +1,6 @@
# 线程类型导致的栈溢出
在Rust中我们不太容易遇到栈溢出因为默认栈还挺大的而且大的数据往往存在堆上(动态增长),但是一旦遇到该如何处理?先来看段代码:
Rust 中,我们不太容易遇到栈溢出,因为默认栈还挺大的,而且大的数据往往存在堆上(动态增长),但是一旦遇到该如何处理?先来看段代码:
```rust
#![feature(test)]
extern crate test;
@ -26,7 +26,7 @@ thread 'tests::it_works' has overflowed its stack
fatal runtime error: stack overflow
```
Bang很不幸遇到了百年一遇的栈溢出错误再来试试`cargo bench`,竟然通过了测试,这是什么原因?为何`cargo test`和`cargo bench`拥有完全不同的行为这就要从Rust的栈原理讲起。
Bang很不幸遇到了百年一遇的栈溢出错误再来试试`cargo bench`,竟然通过了测试,这是什么原因?为何`cargo test`和`cargo bench`拥有完全不同的行为?这就要从 Rust 的栈原理讲起。
首先看看`stack`数组,它的大小是`8 × 2 × 512 × 512 = 4 MiB`,嗯,很大,崩溃也正常(读者说,正常,只是作者你不太正常。。).
@ -45,7 +45,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
```
Bingo, 成功了,最后再补充点测试的背景知识:
> cargo test为何使用新线程因为它需要并行的运行测试用例与之相反cargo bench只需要顺序的执行因此main线程足矣
> `cargo test`为何使用新线程?因为它需要并行的运行测试用例,与之相反,`cargo bench`只需要顺序的执行因此main线程足矣

@ -1,5 +1,5 @@
# 失效的可变性
众所周知Rust是一门安全性非常强的系统级语言其中显式的设置变量可变性是安全性的重要组成部分。按理来说变量可变不可变在设置时就已经决定了但是你遇到过可变变量在某些情况失效变成不可变吗
众所周知 Rust 是一门安全性非常强的系统级语言,其中,显式的设置变量可变性,是安全性的重要组成部分。按理来说,变量可变不可变在设置时就已经决定了,但是你遇到过可变变量在某些情况失效,变成不可变吗?
先来看段正确的代码:
```rust
@ -125,4 +125,4 @@ fn main() {
根据之前的观察和上面的小提示,可以得出一个结论:**可变性的真正含义是你对目标对象的独占修改权**。在实际项目中,偶尔会遇到比上述代码更复杂的可变性情况,记住这个结论,有助于我们拨云见日,直达本质。
学习就是不断接近和认识事物本质的过程对于Rust语言的学习亦是如此。
学习,就是不断接近和认识事物本质的过程,对于 Rust 语言的学习亦是如此。

@ -10,7 +10,7 @@ for i in 0..v.len() {
}
```
我们的目的是创建一个无限增长的数组,往里面插入`0..`(看不懂该表达式的同学请查阅https://course.rs)的数值序列。
我们的目的是创建一个无限增长的数组,往里面插入`0..`(看不懂该表达式的同学请查阅[流程控制](https://course.rs/basic/flow-control.html))的数值序列。
看起来上面代码可以完成,因为随着数组不停增长,`v.len()`也会不停变大,但是事实上真的如此吗?

Loading…
Cancel
Save