Update(pitfalls): unified format 8

pull/509/head
Allan Downey 3 years ago
parent 8e41754cf3
commit 2fb33f47da

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

@ -3,15 +3,18 @@
Rust 一道独特的靓丽风景就是生命周期,也是反复折磨新手的最大黑手,就连老手,可能一不注意就会遇到一些生命周期上的陷阱,例如闭包上使用引用。
## 一段简单的代码
先来看一段简单的代码:
```rust
fn fn_elision(x: &i32) -> &i32 { x }
fn fn_elision(x: &i32) -> &i32 { x }
let closure_slision = |x: &i32| -> &i32 { x };
```
乍一看,这段代码比古天乐还平平无奇,能有什么问题呢?来,走两圈试试:
```console
error: lifetime may not live long enough
error: lifetime may not live long enough
--> src/main.rs:39:39
|
39 | let closure = |x: &i32| -> &i32 { x }; // fails
@ -23,12 +26,14 @@ error: lifetime may not live long enough
咦?竟然报错了,明明两个一模一样功能的函数,一个正常编译,一个却报错,错误原因是编译器无法推测返回的引用和传入的引用谁活得更久!
真的是非常奇怪的错误,学过[Rust生命周期](https://course.rs/advance/lifetime/basic.html)的读者应该都记得这样一条生命周期消除规则: **如果函数参数中只有一个引用类型,那该引用的生命周期会被自动分配给所有的返回引用**。我们当前的情况完美符合,`fn_elision`函数的顺利编译通过,就充分说明了问题。
真的是非常奇怪的错误,学过[Rust 生命周期](https://course.rs/advance/lifetime/basic.html)的读者应该都记得这样一条生命周期消除规则: **如果函数参数中只有一个引用类型,那该引用的生命周期会被自动分配给所有的返回引用**。我们当前的情况完美符合,`fn_elision`函数的顺利编译通过,就充分说明了问题。
那为何闭包就出问题了?
## 一段复杂的代码
为了验证闭包无法应用生命周期消除规则,再来看一个复杂一些的例子:
```rust
use std::marker::PhantomData;
@ -71,6 +76,7 @@ fn main() {
```
该例子之所以这么复杂,纯粹是为了证明闭包上生命周期会失效,读者大大轻拍:) 编译后,不出所料的报错了:
```console
error: lifetime may not live long enough
--> src/main.rs:32:36
@ -83,17 +89,21 @@ error: lifetime may not live long enough
```
## 深入调查
一模一样的报错,说明在这种情况下,生命周期的消除规则也没有生效,看来事情确实不简单,我眉头一皱,决定深入调查,最后还真翻到了一些讨论,经过整理后,大概分享给大家。
首先给出一个结论:**这个问题,可能很难被解决,建议大家遇到后,还是老老实实用正常的函数,不要秀闭包了**。
对于函数的生命周期而言,它的消除规则之所以能生效是因为它的生命周期完全体现在签名的引用类型上,在函数体中无需任何体现:
```rust
fn fn_elision(x: &i32) -> &i32 {..}
```
因此编译器可以做各种编译优化,也很容易根据参数和返回值进行生命周期的分析,最终得出消除规则。
可是闭包,并没有函数那么简单,它的生命周期分散在参数和闭包函数体中(主要是它没有确切的返回值签名)
```rust
let closure_slision = |x: &i32| -> &i32 { x };
```
@ -103,6 +113,8 @@ let closure_slision = |x: &i32| -> &i32 { x };
由于上述原因(当然,实际情况复杂的多) Rust 语言开发者其实目前是有意为之,针对函数和闭包实现了两种不同的生命周期消除规则。
## 总结
虽然我言之凿凿,闭包的生命周期无法解决,但是未来谁又知道呢。最大的可能性就是之前开头那种简单的场景,可以被自动识别和消除。
总之,如果有这种需求,还是像古天乐一样做一个平平无奇的男人,老老实实使用函数吧。
总之,如果有这种需求,还是像古天乐一样做一个平平无奇的男人,老老实实使用函数吧。

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

@ -1,8 +1,11 @@
# 无处不在的迭代器
Rust 的迭代器无处不在,直至你在它上面栽了跟头,经过深入调查才发现:哦,原来是迭代器的锅。不信的话,看看这个报错你能想到是迭代器的问题吗: `borrow of moved value: words`.
## 报错的代码
以下的代码非常简单,用来统计文本中字词的数量,并打印出来:
```rust
fn main() {
let s = "hello world";
@ -13,6 +16,7 @@ fn main() {
```
四行代码,行云流水,一气呵成,且看成效:
```console
error[E0382]: borrow of moved value: `words`
--> src/main.rs:5:21
@ -26,13 +30,17 @@ error[E0382]: borrow of moved value: `words`
```
世事难料,我以为只有的生命周期、闭包才容易背叛革命,没想到一个你浓眉大眼的`count`方法也背叛革命。从报错来看,是因为`count`方法拿走了`words`的所有权,来看看签名:
```rust
fn count(self) -> usize
```
从签名来看,编译器的报错是正确的,但是为什么?为什么一个简单的标准库`count`方法就敢拿走所有权?
## 迭代器回顾
在[迭代器](../advance/functional-programing/iterator.md#消费者与适配器)章节中,我们曾经学习过两个概念:迭代器适配器和消费者适配器,前者用于对迭代器中的元素进行操作,最终生成一个新的迭代器,例如`map`、`filter`等方法;而后者用于消费掉迭代器,最终产生一个结果,例如`collect`方法, 一个典型的示例如下:
```rust
let v1: Vec<i32> = vec![1, 2, 3];
@ -44,9 +52,11 @@ assert_eq!(v2, vec![2, 3, 4]);
在其中,我们还提到一个细节,消费者适配器会拿走迭代器的所有权,那么这个是否与我们最开始碰到的问题有关系?
## 深入调查
要解释这个问题,必须要找到`words`是消费者适配器的证据,因此我们需要深入源码进行查看。
其实。。也不需要多深,只要进入`words`的源码,就能看出它属于`Iterator`特征,那说明`split`方法产生了一个迭代器?再来看看:
```rust
pub fn split<'a, P>(&'a self, pat: P) -> Split<'a, P>
where
@ -60,9 +70,10 @@ where
本身问题不复杂,但是在**解决方法上,可能还有点在各位客官的意料之外**,且看下文。
## 最 rusty 的解决方法
你可能会想用`collect`来解决这个问题,先收集成一个集合,然后进行统计。当然此方法完全可行,但是很不`rusty`(很符合 rust 规范、潮流的意思),以下给出最`rusty`的解决方案:
## 最rusty的解决方法
你可能会想用`collect`来解决这个问题,先收集成一个集合,然后进行统计。当然此方法完全可行,但是很不`rusty`(很符合rust规范、潮流的意思),以下给出最`rusty`的解决方案:
```rust
let words = s.split(",");
let n = words.clone().count();
@ -74,9 +85,11 @@ let n = words.clone().count();
大家且听我慢慢道来,事实上,在 Rust 中`clone`不总是性能低下的代名词,因为`clone`的行为完全取决于它的具体实现。
#### 迭代器的`clone`代价
对于迭代器而言,它其实并不需要持有数据才能进行迭代,事实上它包含一个引用,该引用指向了保存在堆上的数据,而迭代器自身的结构是保存在栈上。
因此对迭代器的`clone`仅仅是复制了一份栈上的简单结构,性能非常高效,例如:
```rust
pub struct Split<'a, T: 'a, P>
where
@ -102,6 +115,8 @@ where
以上代码实现了对`Split`迭代器的克隆,可以看出,底层的的数组`self.v`并没有被克隆而是简单的复制了一个引用,依然指向了底层的数组`&[T]`,因此这个克隆非常高效。
## 总结
看起来是无效借用导致的错误,实际上是迭代器被消费了导致的问题,这说明 Rust 编译器虽然会告诉你错误原因,但是这个原因不总是根本原因。我们需要一双慧眼和勤劳的手,来挖掘出这个宝藏,最后为己所用。
同时,克隆在 Rust 中也并不总是**bad guy**的代名词,有的时候我们可以大胆去使用,当然前提是了解你的代码场景和具体的`clone`实现,这样你也能像文中那样作出非常`rusty`的选择。
同时,克隆在 Rust 中也并不总是**bad guy**的代名词,有的时候我们可以大胆去使用,当然前提是了解你的代码场景和具体的`clone`实现,这样你也能像文中那样作出非常`rusty`的选择。

@ -1,12 +1,15 @@
# 不太勤快的迭代器
迭代器,在 Rust 中是一个非常耀眼的存在,它光鲜亮丽,它让 Rust 大道至简,它备受用户的喜爱。可是,它也是懒惰的,不信?一起来看看。
## for循环 vs 迭代器
## for 循环 vs 迭代器
在迭代器学习中,我们提到过迭代器在功能上可以替代循环,性能上略微优于循环(避免边界检查),安全性上优于循环,因此在 Rust 中,迭代器往往都是更优的选择,前提是迭代器得发挥作用。
在下面代码中,分别是使用`for`循环和迭代器去生成一个`HashMap`。
使用循环:
```rust
use std::collections::HashMap;
#[derive(Debug)]
@ -21,12 +24,13 @@ fn main() {
for a in accounts {
resolvers.entry(a.id).or_insert(Vec::new()).push(a);
}
println!("{:?}",resolvers);
}
```
使用迭代器:
```rust
let mut resolvers = HashMap::new();
accounts.into_iter().map(|a| {
@ -39,6 +43,7 @@ println!("{:?}",resolvers);
```
#### 预料之外的结果
两端代码乍一看(很多时候我们快速浏览代码的时候,不会去细看)都很正常, 运行下试试:
- `for`循环很正常,输出`{2: [Account { id: 2 }], 1: [Account { id: 1 }], 3: [Account { id: 3 }]}`
@ -47,12 +52,15 @@ println!("{:?}",resolvers);
在继续深挖之前,我们先来简单回顾下迭代器。
## 回顾下迭代器
在迭代器章节中,我们曾经提到过,迭代器的[适配器](../advance/functional-programing/iterator.md#消费者与适配器)分为两种:消费者适配器和迭代器适配器,前者用来将一个迭代器变为指定的集合类型,往往通过`collect`实现;后者用于生成一个新的迭代器,例如上例中的`map`。
还提到过非常重要的一点: **迭代器适配器都是懒惰的,只有配合消费者适配器使用时,才会进行求值**.
## 懒惰是根因
在我们之前的迭代器示例中,只有一个迭代器适配器`map`:
```rust
accounts.into_iter().map(|a| {
resolvers
@ -68,14 +76,16 @@ accounts.into_iter().map(|a| {
自然,我们的插值计划也失败了。
> 事实上IDE和编译器都会对这种代码给出警告iterators are lazy and do nothing unless consumed
> 事实上IDE 和编译器都会对这种代码给出警告iterators are lazy and do nothing unless consumed
## 解决办法
原因非常清晰,如果读者还有疑惑,建议深度了解下上面给出的迭代器链接,我们这里就不再赘述。
下面列出三种合理的解决办法:
1. 不再使用迭代器适配器`map`,改成`for_each`:
```rust
let mut resolvers = HashMap::new();
accounts.into_iter().for_each(|a| {
@ -89,6 +99,7 @@ accounts.into_iter().for_each(|a| {
但是,相关的文档也友善的提示了我们,除非作为链式调用的收尾,否则更建议使用`for`循环来处理这种情况。哎,忙忙碌碌,又回到了原点,不禁让人感叹:天道有轮回。
2. 使用消费者适配器`collect`来收尾,将`map`产生的迭代器收集成一个集合类型:
```rust
let resolvers: HashMap<_, _> = accounts
.into_iter()
@ -99,6 +110,7 @@ let resolvers: HashMap<_, _> = accounts
嗯,还挺简洁,挺`rusty`.
3. 使用`fold`,语义表达更强:
```rust
let resolvers = account.into_iter().fold(HashMap::new(), |mut resolvers, a|{
resolvers.entry(a.id).or_insert(Vec::new).push(a);
@ -107,6 +119,5 @@ let resolvers = account.into_iter().fold(HashMap::new(), |mut resolvers, a|{
```
## 总结
在使用迭代器时,要清晰的认识到需要用到的方法是迭代型还是消费型适配器,如果一个调用链中没有以消费型适配器结尾,就需要打起精神了,也许,不远处就是一个陷阱在等你跳:)
在使用迭代器时,要清晰的认识到需要用到的方法是迭代型还是消费型适配器,如果一个调用链中没有以消费型适配器结尾,就需要打起精神了,也许,不远处就是一个陷阱在等你跳:)

@ -1,13 +1,15 @@
# 线程间传递消息导致主线程无法结束
本篇陷阱较短,主要解决新手在多线程间传递消息时可能会遇到的一个问题:主线程会一直阻塞,无法结束。
Rust 标准库中提供了一个消息通道,非常好用,也相当简单明了,但是但是在使用起来还是可能存在坑:
```rust
use std::sync::mpsc;
fn main() {
use std::thread;
let (send, recv) = mpsc::channel();
let num_threads = 3;
for i in 0..num_threads {
@ -17,7 +19,7 @@ fn main() {
println!("thread {:?} finished", i);
});
}
for x in recv {
println!("Got: {}", x);
}
@ -26,6 +28,7 @@ fn main() {
```
以上代码看起来非常正常,运行下试试:
```console
thread 0 finished
thread 1 finished
@ -44,12 +47,13 @@ Got: 2
来分析下代码,每一个子线程都从`send`获取了一个拷贝,然后该拷贝在子线程结束时自动被`drop`,看上去没问题啊。等等,好像`send`本身并没有被`drop`,因为`send`要等到`main`函数结束才会被`drop`,那么代码就陷入了一个尴尬的境地:`main`函数要结束需要`for`循环结束,`for`循环结束需要`send`被`drop`,而`send`要被`drop`需要`main`函数结束。。。
破局点只有一个,那就是主动`drop`掉`send`,这个简单,使用`std::mem::drop`函数即可,得益于`prelude`,我们只需要使用`drop`:
```rust
use std::sync::mpsc;
fn main() {
use std::thread;
let (send, recv) = mpsc::channel();
let num_threads = 3;
for i in 0..num_threads {
@ -60,9 +64,9 @@ fn main() {
println!("thread {:?} finished", i);
});
}
drop(send);
for x in recv {
for x in recv {
println!("Got: {}", x);
}
println!("finished iterating");
@ -72,4 +76,5 @@ fn main() {
此时再运行,主线程将顺利结束。
## 总结
本文总结了一个新手在使用消息通道时常见的错误,那就是忘记处理创建通道时得到的发送者,最后由于该发送者的存活导致通道无法被关闭,最终主线程阻塞,造成程序错误。

@ -3,14 +3,17 @@
相信大家都听说过**重构一时爽,一直重构一直爽**的说法,私以为这种说法是很有道理的,不然技术团队绩效从何而来?但是,在 Rust 中,重构可能就不是那么爽快的事了,不信?咱们来看看。
## 欣赏下报错
很多时候,错误也是一种美,但是当这种错误每天都能见到时(呕):
```css
error[E0499]: cannot borrow `*self` as mutable more than once at a time
error[e0499]: cannot borrow ` * self` as mutable more than once at a time;
```
虽然这一类错误长得一样,但是我这里的错误可能并不是大家常遇到的那些妖艳错误,废话不多说,一起来看看。
## 重构前的正确代码
```rust
struct Test {
a : u32,
@ -32,6 +35,7 @@ impl Test {
答案要从很久很久之前开始(啊哒~~~由于我太啰嗦,被正义群众来了一下,那咱现在开始长话短说,直接进入主题)。
#### 正确代码为何不报错?
虽然从表面来看,`a`和`b`都可变引用了`self`,但是 Rust 的编译器在很多时候都足够聪明,它发现我们其实仅仅引用了同一个结构体中的不同字段,因此完全可以将其的借用权分离开来。
因此,虽然我们不能同时对整个结构体进行可变引用,但是我们可以分别对结构体中的不同字段进行可变引用,当然,一个字段至多也只能存在一个可变引用,这个最基本的所有权规则还是不能违反的。变量`a`引用结构体字段`a`,变量`b`引用结构体字段`b`,从底层来说,这种方式也不会造成两个可变引用指向了同一块内存。
@ -39,7 +43,9 @@ impl Test {
至此,正确代码我们已经挖掘完毕,再来看看重构后的错误代码。
## 重构后的错误代码
由于领导说我们这个函数没办法复用,那就敷衍一下呗:
```rust
struct Test {
a : u32,
@ -63,6 +69,7 @@ impl Test {
既然领导说了,咱照做,反正他也没说怎么个复用法,咱就来个简单的,把`a`的递增部分复用下。
代码说实话。。。更丑了,但是更强了吗?
```console
error[E0499]: cannot borrow `*self` as mutable more than once at a time
--> src/main.rs:14:9
@ -78,16 +85,19 @@ error[E0499]: cannot borrow `*self` as mutable more than once at a time
嗯,最开始提到的错误,它终于出现了。
## 大聪明编译器
为什么?明明之前还是正确的代码,就因为放入函数中就报错了?我们先从一个简单的理解谈起,当然这个理解也是浮于表面的,等会会深入分析真实的原因。
之前讲到 Rust 编译器挺聪明,可以识别到引用到不同的结构体字段,因此不会报错。但是现在这种情况下,编译器又不够聪明了,一旦放入函数中,编译器将无法理解我们对`self`的使用:它仅仅用到了一个字段,而不是整个结构体。
之前讲到 Rust 编译器挺聪明,可以识别到引用到不同的结构体字段,因此不会报错。但是现在这种情况下,编译器又不够聪明了,一旦放入函数中,编译器将无法理解我们对`self`的使用:它仅仅用到了一个字段,而不是整个结构体。
因此它会简单的认为,这个结构体作为一个整体被可变借用了,产生两个可变引用,一个引用整个结构体,一个引用了结构体字段`b`,这两个引用存在重叠的部分,最终导致编译错误。
## 被冤枉的编译器
在工作生活中,我们无法理解甚至错误的理解一件事,有时是因为层次不够导致的。同样,对于本文来说,也是因为我们对编译器的所知不够,才冤枉了它,还给它起了一个屈辱的“大聪明”外号。
#### 深入分析
> 如果只改变相关函数的实现而不改变它的签名,那么不会影响编译的结果
何为相关函数?当函数`a`调用了函数`b`,那么`b`就是`a`的相关函数。
@ -95,6 +105,7 @@ error[E0499]: cannot borrow `*self` as mutable more than once at a time
上面这句是一条非常重要的编译准则,意思是,对于编译器来说,只要函数签名没有变,那么任何函数实现的修改都不会影响已有的编译结果(前提是函数实现没有错误- , -)。
以前面的代码为例:
```rust
fn increase_a (&mut self) {
self.a += 1;
@ -110,14 +121,16 @@ fn increase(&mut self) {
虽然`increase_a`在函数实现中没有访问`self.b`字段,但是它的签名允许它访问`b`,因此违背了借用规则。事实上,该函数有没有访问`b`不重要,**因为编译器在这里只关心签名,签名存在可能性,那么就立刻报出错误**。
为何会有这种编译器行为,主要有两个原因:
1. 一般来说,我们希望编译器有能力独立的编译每个函数,而无需深入到相关函数的内部实现,因为这样做会带来快得多的编译速度。
2. 如果没有这种保证,那么在实际项目开发中,我们会特别容易遇到各种错误。 假设我们要求编译器不仅仅关注相关函数的签名,还要深入其内部关注实现,那么由于 Rust 严苛的编译规则,当你修改了某个函数内部实现的代码后,可能会引起使用该函数的其它函数的各种错误!对于大型项目来说,这几乎是不可接受的!
然后,我们的借用类型这么简单,编译器有没有可能针对这种场景,在现有的借用规则之外增加特殊规则?答案是否定的,由于 Rust 语言的设计哲学:特殊规则的加入需要慎之又慎,而我们的这种情况其实还蛮好解决的,因此编译器不会为此新增规则。
## 解决办法
在深入分析中,我们提到一条重要的规则,要影响编译行为,就需要更改相关函数的签名,因此可以修改`increase_a`的签名:
```rust
fn increase_a (a :&mut u32) {
*a += 1;
@ -132,7 +145,6 @@ fn increase(&mut self) {
此时,`increase_a`这个相关函数,不再使用`&mut self`作为签名,而是获取了结构体中的字段`a`,此时编译器又可以清晰的知道:函数`increase_a`和变量`b`分别引用了结构体中的不同字段,因此可以编译通过。
当然,除了修改相关函数的签名,你还可以修改调用者的实现:
```rust
@ -144,9 +156,10 @@ fn increase(&mut self) {
在这里,我们不再单独声明变量`b`,而是直接调用`self.b+=1`进行递增,根据借用生命周期[NLL](https://course.rs/advance/lifetime/advance.html#nllnon-lexical-lifetime)的规则,第一个可变借用`self.increase_a()`的生命周期随着方法调用的结束而结束,那么就不会影响`self.b += 1`中的借用。
## 闭包中的例子
再来看一个使用了闭包的例子:
```rust
use tokio::runtime::Runtime;
@ -183,6 +196,7 @@ impl ServerRuntime {
```
代码中使用了`tokio`,在`increase_connections_count`函数中启动了一个异步任务,并且等待它的完成。这个函数中分别引用了`self`中的不同字段:`runtime`和`server`,但是可能因为闭包的原因,编译器没有像本文最开始的例子中那样聪明,并不能识别这两个引用仅仅引用了同一个结构体的不同部分,因此报错了:
```console
error[E0501]: cannot borrow `self.runtime` as mutable because previous closure requires unique access
--> the_little_things\src\main.rs:28:9
@ -201,7 +215,9 @@ error[E0501]: cannot borrow `self.runtime` as mutable because previous closure r
```
#### 解决办法
解决办法很粗暴,既然编译器不能理解闭包中的引用是不同的,那么我们就主动告诉它:
```rust
pub fn increase_connections_count(&mut self) {
let runtime = &mut self.runtime;
@ -215,6 +231,7 @@ pub fn increase_connections_count(&mut self) {
上面通过变量声明的方式,在闭包外声明了两个变量分别引用结构体`self`的不同字段,这样一来,编译器就不会那么笨,编译顺利通过。
你也可以这么写:
```rust
pub fn increase_connections_count(&mut self) {
let ServerRuntime { runtime, server } = self;
@ -227,6 +244,8 @@ pub fn increase_connections_count(&mut self) {
当然,如果难以解决,还有一个笨办法,那就是将`server`和`runtime`分离开来,不要放在一个结构体中。
## 总结
心中有剑,手中无剑,是武学至高境界。
本文列出的那条编译规则,在未来就将是大家心中的那把剑,当这些心剑招式足够多时,量变产生质变,终将天下无敌。
本文列出的那条编译规则,在未来就将是大家心中的那把剑,当这些心剑招式足够多时,量变产生质变,终将天下无敌。

@ -1,6 +1,7 @@
# 线程类型导致的栈溢出
在 Rust 中,我们不太容易遇到栈溢出,因为默认栈还挺大的,而且大的数据往往存在堆上(动态增长),但是一旦遇到该如何处理?先来看段代码:
```rust
#![feature(test)]
extern crate test;
@ -17,7 +18,8 @@ mod tests {
```
以上代码是一个测试模块,它在堆上生成了一个数组`stack`,初步看起来数组挺大的,先尝试运行下`cargo test`:
> 你很可能会遇到`#![feature(test)]`错误,因为该特性目前只存在`Rust Nightly`版本上,具体解决方法见[Rust语言圣经](https://course.rs/appendix/rust-version.html#在指定目录使用rust-nightly)
> 你很可能会遇到`#![feature(test)]`错误,因为该特性目前只存在`Rust Nightly`版本上,具体解决方法见[Rust 语言圣经](https://course.rs/appendix/rust-version.html#在指定目录使用rust-nightly)
```console
running 1 test
@ -30,22 +32,21 @@ Bang很不幸遇到了百年一遇的栈溢出错误再来试试`cargo
首先看看`stack`数组,它的大小是`8 × 2 × 512 × 512 = 4 MiB`,嗯,很大,崩溃也正常(读者说,正常,只是作者你不太正常。。).
其次,`cargo test`和`cargo bench`,前者运行在一个新创建的线程上,而后者运行在**main线程上**.
其次,`cargo test`和`cargo bench`,前者运行在一个新创建的线程上,而后者运行在**main 线程上**.
最后,`main`线程由于是老大,所以资源比较多,拥有令其它兄弟艳羡不已的`8MB`栈大小,而其它新线程只有区区`2MB`栈大小(取决于操作系统,`linux`是`2MB`,其它的可能更小),再对比我们的`stack`大小,不崩溃就奇怪了。
因此,你现在明白,为何`cargo test`不能运行,而`cargo bench`却可以欢快运行。
如果实在想要增大栈的默认大小,以通过该测试,你可以这样运行:`RUST_MIN_STACK=8388608 cargo test`,结果如下:
```console
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```
Bingo, 成功了,最后再补充点测试的背景知识:
> `cargo test`为何使用新线程?因为它需要并行的运行测试用例,与之相反,`cargo bench`只需要顺序的执行因此main线程足矣
Bingo, 成功了,最后再补充点测试的背景知识:
> `cargo test`为何使用新线程?因为它需要并行的运行测试用例,与之相反,`cargo bench`只需要顺序的执行,因此 main 线程足矣

@ -1,7 +1,9 @@
# 失效的可变性
众所周知 Rust 是一门安全性非常强的系统级语言,其中,显式的设置变量可变性,是安全性的重要组成部分。按理来说,变量可变不可变在设置时就已经决定了,但是你遇到过可变变量在某些情况失效,变成不可变吗?
先来看段正确的代码:
```rust
#[derive(Debug)]
struct A {
@ -22,19 +24,20 @@ fn main() {
let b: B = B{ f1: 3, a: &mut a };
// 但是b中的字段a可以变
b.a.f1 += 1;
println!("b is {:?} ", &b);
}
```
在这里,虽然变量`b`被设置为不可变,但是`b`的其中一个字段`a`被设置为可变的结构体,因此我们可以通过`b.a.f1 += 1`来修改`a`的值。
也许有人还不知道这种部分可变性的存在,不过没关系,因为马上就不可变了:)
也许有人还不知道这种部分可变性的存在,不过没关系,因为马上就不可变了:)
- 结构体可变时,里面的字段都是可变的,例如`&mut a`
- 结构体不可变时,里面的某个字段可以单独设置为可变,例如`b.a`
在理解了上面两条简单规则后,来看看下面这段代码:
```rust
#[derive(Debug)]
struct A {
@ -62,12 +65,13 @@ fn main() {
// b is immutable
let b: B = B{ f1: 3, a: &mut a };
b.changeme();
println!("b is {:?} ", &b);
}
```
这段代码,仅仅做了一个小改变,不再直接修改`b.a`,而是通过调用`b`上的方法去修改其中的`a`,按理说不会有任何区别。因此我预言:通过方法调用跟直接调用不应该有任何区别,运行验证下:
```console
error[E0594]: cannot assign to `self.a.f1`, which is behind a `&` reference
--> src/main.rs:18:9
@ -81,11 +85,13 @@ error[E0594]: cannot assign to `self.a.f1`, which is behind a `&` reference
啪,又被打脸了。我说我是大意了,没有闪,大家信不?反正马先生应该是信的:D
## 简单分析
观察第一个例子,我们调用的`b.a`实际上是用`b`的值直接调用的,在这种情况下,由于所有权规则,编译器可以认定,只有一个可变引用指向了`a`,因此这种使用是非常安全的。
但是,在第二个例子中,`b`被藏在了`&`后面,根据所有权规则,同时可能存在多个`b`的借用,那么就意味着可能会存在多个可变引用指向`a`,因此编译器就拒绝了这段代码。
事实上如果你将第一段代码的调用改成:
```rust
let b: &B = &B{ f1: 3, a: &mut a };
b.a.f1 += 1;
@ -94,7 +100,9 @@ b.a.f1 += 1;
一样会报错!
## 一个练习
结束之前再来一个练习,稍微有点绕,大家品味品味:
```rust
#[derive(Debug)]
struct A {
@ -114,7 +122,7 @@ fn main() {
let b: B = B{ f1: 3, a: &mut a };
b.a.f1 += 1;
a.f1 = 10;
println!("b is {:?} ", &b);
}
```
@ -125,4 +133,5 @@ fn main() {
根据之前的观察和上面的小提示,可以得出一个结论:**可变性的真正含义是你对目标对象的独占修改权**。在实际项目中,偶尔会遇到比上述代码更复杂的可变性情况,记住这个结论,有助于我们拨云见日,直达本质。
学习,就是不断接近和认识事物本质的过程,对于 Rust 语言的学习亦是如此。
学习,就是不断接近和认识事物本质的过程,对于 Rust 语言的学习亦是如此。

@ -1,6 +1,7 @@
# for循环中使用外部数组
# for 循环中使用外部数组
一般来说,`for`循环能做到的,`while`也可以,反之亦然,但是有一种情况,还真不行,先来看代码:
```rust
let mut v = vec![1,2,3];
@ -22,9 +23,10 @@ for i in 0..v.len() {
输出很清晰的表明,只新插入了三个元素:`0..=2`,刚好是`v`的初始长度。
这是因为:**在for循环中,`v.len`只会在循环伊始之时进行求值,之后就一直使用该值**。
这是因为:**在 for 循环中,`v.len`只会在循环伊始之时进行求值,之后就一直使用该值**。
行,问题算是清楚了,那该如何解决呢,我们可以使用`while`循环,该循环与`for`相反,每次都会重新求值:
```rust
let mut v = vec![1,2,3];
@ -37,4 +39,3 @@ while i < v.len() {
```
友情提示,在你运行上述代码时,千万要及时停止,否则会`Boom` - 炸翻控制台。

Loading…
Cancel
Save