add陷阱系列

pull/107/head
sunface 3 years ago
parent 966ccfb9be
commit 13901602bf

BIN
.DS_Store vendored

Binary file not shown.

@ -70,9 +70,22 @@
- [Rc与RefCell(todo)](advance/smart-pointer/rc-refcell.md)
- [自引用与内存泄漏(todo)](advance/smart-pointer/self-referrence.md)
- [全局变量 todo](advance/global-variable.md)
- [多线程 todo](advance/multi-threads/intro.md)
- [线程管理(todo)](advance/multi-threads/thread.md)
- [消息传递(todo)](advance/multi-threads/message-passing.md)
- [数据共享Arc、Mutex、Rwlock(todo)](advance/multi-threads/ref-counter-lock.md)
- [数据竞争(todo)](advance/multi-threads/races.md)
- [Send、Sync(todo)](advance/multi-threads/send-sync.md)
## 专题内容,每个专题都配套一个小型项目进行实践
- [Rust陷阱系列](pitfalls/index.md)
- [for循环中使用外部数组](pitfalls/use-vec-in-for.md)
- [线程类型导致的栈溢出](pitfalls/stack-overflow.md)
- [算术溢出导致的panic](pitfalls/arithmetic-overflow.md)
- [闭包中奇怪的生命周期](pitfalls/closure-with-lifetime.md)
- [可变变量不可变?](pitfalls/the-disabled-mutability.md)
- [可变借用失败引发的深入思考](pitfalls/multiple-mutable-references.md)
- [Rust最佳实践 todo](practice/intro.md)
- [一些写代码的技巧 todo](practice/coding-tips.md)
- [最佳实践 todo](practice/best-pratice.md)
@ -97,13 +110,6 @@
- [写时拷贝Cow](traits/cow.md)
- [Eq](traits/eq.md)
- [多线程 todo](multi-threads/intro.md)
- [线程管理(todo)](multi-threads/thread.md)
- [消息传递(todo)](multi-threads/message-passing.md)
- [数据共享Arc、Mutex、Rwlock(todo)](multi-threads/ref-counter-lock.md)
- [数据竞争(todo)](multi-threads/races.md)
- [Send、Sync(todo)](multi-threads/send-sync.md)
- [深入内存 todo](memory/intro.md)
- [指针和引用(todo)](memory/pointer-ref.md)
- [未初始化内存(todo)](memory/uninit.md)

@ -0,0 +1,62 @@
# 算术溢出导致的panic
在Rust中溢出后的数值被截断是很正常的:
```rust
let x: u16 = 65535;
let v = x as u8;
println!("{}", v)
```
最终程序会输出`255`, 因此大家可能会下意识地就觉得算数操作在Rust中只会导致结果的不正确并不会导致异常。但是实际上如果是因为算术操作符导致的溢出就会让整个程序panic:
```rust
fn main() {
let x: u8 = 10;
let v = x + u8::MAX;
println!("{}", v)
}
```
输出结果如下:
```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;
let v = x.checked_add(u8::MAX).unwrap_or(0);
println!("{}", v)
}
```
也许你会觉得本章内容其实算不上什么陷阱,但是在实际项目快速迭代中,越是不起眼的地方越是容易出错:
```rust
fn main() {
let v = production_rate_per_hour(5);
println!("{}", v);
}
pub fn production_rate_per_hour(speed: u8) -> f64 {
let cph: u8 = 221;
match speed {
1..=4 => (speed * cph) as f64,
5..=8 => (speed * cph) as f64 * 0.9,
9..=10 => (speed * cph) as f64 * 0.77,
_ => 0 as f64,
}
}
pub fn working_items_per_minute(speed: u8) -> u32 {
(production_rate_per_hour(speed) / 60 as f64) as u32
}
```
上述代码中,`speed * cph`就会直接panic:
```console
thread 'main' panicked at 'attempt to multiply with overflow', src/main.rs:10:18
```
是不是还藏的挺隐蔽的因此大家在Rust中做数学运算时要多留一个心眼免得上了生产才发现问题所在。或者你也可以做好单元测试:)

@ -0,0 +1,108 @@
# 闭包上奇怪的生命周期
Rust一道独特的靓丽风景就是生命周期也是反复折磨新手的最大黑手就连老手可能一不注意就会遇到一些生命周期上的陷阱例如闭包上使用引用。
## 一段简单的代码
先来看一段简单的代码:
```rust
fn fn_elision(x: &i32) -> &i32 { x }
let closure_slision = |x: &i32| -> &i32 { x };
```
乍一看,这段代码比古天乐还平平无奇,能有什么问题呢?来,走两圈试试:
```console
error: lifetime may not live long enough
--> src/main.rs:39:39
|
39 | let closure = |x: &i32| -> &i32 { x }; // fails
| - - ^ returning this value requires that `'1` must outlive `'2`
| | |
| | let's call the lifetime of this reference `'2`
| let's call the lifetime of this reference `'1`
```
咦?竟然报错了,明明两个一模一样功能的函数,一个正常编译,一个却报错,错误原因是编译器无法推测返回的引用和传入的引用谁活得更久!
真的是非常奇怪的错误,学过[Rust生命周期](https://github.com/sunface/rust-course/blob/main/src/advance/lifetime/basic.md)的读者应该都记得这样一条生命周期消除规则: **如果函数参数中只有一个引用类型,那该引用的生命周期会被自动分配给所有的返回引用**。我们当前的情况完美符合,`function`函数的顺利编译通过,就充分说明了问题。
那为何闭包就出问题了?
## 一段复杂的代码
为了验证闭包无法应用生命周期消除规则,再来看一个复杂一些的例子:
```rust
use std::marker::PhantomData;
trait Parser<'a>: Sized + Copy {
fn parse(&self, tail: &'a str) -> &'a str {
tail
}
fn wrap(self) -> Wrapper<'a, Self> {
Wrapper {
parser: self,
marker: PhantomData,
}
}
}
#[derive(Copy, Clone)]
struct T<'x> {
int: &'x i32,
}
impl<'a, 'x> Parser<'a> for T<'x> {}
struct Wrapper<'a, P>
where
P: Parser<'a>,
{
parser: P,
marker: PhantomData<&'a ()>,
}
fn main() {
// Error.
let closure_wrap = |parser: T| parser.wrap();
// No error.
fn parser_wrap(parser: T<'_>) -> Wrapper<'_, T<'_>> {
parser.wrap()
}
}
```
该例子之所以这么复杂,纯粹是为了证明闭包上生命周期会失效,读者大大轻拍:) 编译后,不出所料的报错了:
```console
error: lifetime may not live long enough
--> src/main.rs:32:36
|
32 | let closure_wrap = |parser: T| parser.wrap();
| ------ - ^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
| | |
| | return type of closure is Wrapper<'_, T<'2>>
| has type `T<'1>`
```
## 深入调查
一模一样的报错,说明在这种情况下,生命周期的消除规则也没有生效,看来事情确实不简单,我眉头一皱,决定深入调查,最后还真翻到了一些讨论,经过整理后,大概分享给大家。
首先给出一个结论:**这个问题,可能很难被解决,建议大家遇到后,还是老老实实用正常的函数,不要秀闭包了**。
对于函数的生命周期而言,它的消除规则之所以能生效是因为它的生命周期完全体现在签名的引用类型上,在函数体中无需任何体现:
```rust
fn fn_elision(x: &i32) -> &i32 {..}
```
因此编译器可以做各种编译优化,也很容易根据参数和返回值进行生命周期的分析,最终得出消除规则。
可是闭包,并没有函数那么简单,它的生命周期分散在参数和闭包函数体中(主要是它没有确切的返回值签名)
```rust
let closure_slision = |x: &i32| -> &i32 { x };
```
编译器就必须深入到闭包函数体中,去分析和推测生命周期,复杂度因此极具提升:试想一下,编译器该如何从复杂的上下文中分析出参数引用的生命周期和闭包体中生命周期的关系?
由于上述原因(当然,实际情况复杂的多)Rust语言开发者其实目前是有意为之针对函数和闭包实现了两种不同的生命周期消除规则。
## 总结
虽然我言之凿凿,闭包的生命周期无法解决,但是未来谁又知道呢。最大的可能性就是之前开头那种简单的场景,可以被自动识别和消除。
总之,如果有这种需求,还是像古天乐一样做一个平平无奇的男人,老老实实使用函数吧.

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

@ -0,0 +1,6 @@
# 多可变引用
https://oribenshir.github.io/afternoon_rusting//blog/mutable-reference
https://oribenshir.github.io/afternoon_rusting//blog/mutable-reference

@ -0,0 +1,51 @@
# 线程类型导致的栈溢出
在Rust中我们不太容易遇到栈溢出因为默认栈还挺大的而且大的数据往往存在堆上(动态增长),但是一旦遇到该如何处理?先来看段代码:
```rust
#![feature(test)]
extern crate test;
#[cfg(test)]
mod tests {
use test::Bencher;
#[bench]
fn it_works(b: &mut Bencher) {
b.iter(|| { let stack = [[[0.0; 2]; 512]; 512]; });
}
}
```
以上代码是一个测试模块,它在堆上生成了一个数组`stack`,初步看起来数组挺大的,先尝试运行下`cargo test`:
> 你很可能会遇到`#![feature(test)]`错误,因为该特性目前只存在`Rust Nightly`版本上,具体解决方法见[Rust语言圣经](https://course.rs/appendix/rust-version.html#在指定目录使用rust-nightly)
```console
running 1 test
thread 'tests::it_works' has overflowed its stack
fatal runtime error: stack overflow
```
Bang很不幸遇到了百年一遇的栈溢出错误再来试试`cargo bench`,竟然通过了测试,这是什么原因?为何`cargo test`和`cargo bench`拥有完全不同的行为这就要从Rust的栈原理讲起。
首先看看`stack`数组,它的大小是`8 × 2 × 512 × 512 = 4 MiB`,嗯,很大,崩溃也正常(读者说,正常,只是作者你不太正常。。).
其次,`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线程足矣

@ -0,0 +1,128 @@
# 失效的可变性
众所周知Rust是一门安全性非常强的系统级语言其中显式的设置变量可变性是安全性的重要组成部分。按理来说变量可变不可变在设置时就已经决定了但是你遇到过可变变量在某些情况失效变成不可变吗
先来看段正确的代码:
```rust
#[derive(Debug)]
struct A {
f1: u32,
f2: u32,
f3: u32
}
#[derive(Debug)]
struct B<'a> {
f1: u32,
a: &'a mut A,
}
fn main() {
let mut a: A = A{ f1: 0, f2: 1, f3: 2 };
// b不可变
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 {
f1: u32,
f2: u32,
f3: u32
}
#[derive(Debug)]
struct B<'a> {
f1: u32,
a: &'a mut A,
}
impl B<'_> {
// this will not work
pub fn changeme(&self) {
self.a.f1 += 1;
}
}
fn main() {
let mut a: A = A{ f1: 0, f2: 1, f3: 2 };
// 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
|
17 | pub fn changeme(&self) {
| ----- help: consider changing this to be a mutable reference: `&mut self`
18 | self.a.f1 += 1;
| ^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be written
```
啪,又被打脸了。我说我是大意了,没有闪,大家信不?反正马先生应该是信的:D
## 简单分析
观察第一个例子,我们调用的`b.a`实际上是用`b`的值直接调用的,在这种情况下,由于所有权规则,编译器可以认定,只有一个可变引用指向了`a`,因此这种使用是非常安全的。
但是,在第二个例子中,`b`被藏在了`&`后面,根据所有权规则,同时可能存在多个`b`的借用,那么就意味着可能会存在多个可变引用指向`a`,因此编译器就拒绝了这段代码。
事实上如果你将第一段代码的调用改成:
```rust
let b: &B = &B{ f1: 3, a: &mut a };
b.a.f1 += 1;
```
一样会报错!
## 一个练习
结束之前再来一个练习,稍微有点绕,大家品味品味:
```rust
#[derive(Debug)]
struct A {
f1: u32,
f2: u32,
f3: u32
}
#[derive(Debug)]
struct B<'a> {
f1: u32,
a: &'a mut A,
}
fn main() {
let mut a: A = A{ f1: 0, f2: 1, f3: 2 };
let b: B = B{ f1: 3, a: &mut a };
b.a.f1 += 1;
a.f1 = 10;
println!("b is {:?} ", &b);
}
```
小提示:这里`b.a.f1 += 1`和`a.f1 = 10`只能有一个存在,否则就会报错。
## 总结
根据之前的观察和上面的小提示,可以得出一个结论:**可变性的真正含义是你对目标对象的独占修改权**。在实际项目中,偶尔会遇到比上述代码更复杂的可变性情况,记住这个结论,有助于我们拨云见日,直达本质。
学习就是不断接近和认识事物本质的过程对于Rust语言的学习亦是如此。

@ -0,0 +1,40 @@
# for循环中使用外部数组
一般来说,`for`循环能做到的,`while`也可以,反之亦然,但是有一种情况,还真不行,先来看代码:
```rust
let mut v = vec![1,2,3];
for i in 0..v.len() {
v.push(i);
println!("{:?}",v);
}
```
我们的目的是创建一个无限增长的数组,往里面插入`0..`(看不懂该表达式的同学请查阅https://course.rs)的数值序列。
看起来上面代码可以完成,因为随着数组不停增长,`v.len()`也会不停变大,但是事实上真的如此吗?
```console
[1, 2, 3, 0]
[1, 2, 3, 0, 1]
[1, 2, 3, 0, 1, 2]
```
输出很清晰的表明,只新插入了三个元素:`0..=2`,刚好是`v`的初始长度。
这是因为:**在for循环中,`v.len`只会在循环伊始之时进行求值,之后就一直使用该值**。
行,问题算是清楚了,那该如何解决呢,我们可以使用`while`循环,该循环与`for`相反,每次都会重新求值:
```rust
let mut v = vec![1,2,3];
let mut i = 0;
while i < v.len() {
v.push(i);
i+=1;
println!("{:?}",v);
}
```
友情提示,在你运行上述代码时,千万要及时停止,否则会`Boom` - 炸翻控制台。
Loading…
Cancel
Save