From 718c89ea91d9dcfd96b7992e43e84e04aa07426d Mon Sep 17 00:00:00 2001 From: sunface Date: Fri, 31 Dec 2021 17:10:34 +0800 Subject: [PATCH] =?UTF-8?q?add=E6=99=BA=E8=83=BD=E6=8C=87=E9=92=88Box?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 10244 -> 8196 bytes course-book/contents/.DS_Store | Bin 8196 -> 8196 bytes .../contents/advance/smart-pointer/box.md | 194 +++++++++++++++++- 3 files changed, 191 insertions(+), 3 deletions(-) diff --git a/.DS_Store b/.DS_Store index 03f76844088a7698119920224e3b4e04df32936f..4fa248e9937e720b46b44870a1d8c03c50936532 100644 GIT binary patch delta 244 zcmZn(XmOBWU|?W$DortDU;r^WfEYvza8E20o2Vx_*+7Lw63Ax|0%As>I0HioL$PO0 zesWSye$r+^4l$O^3LL%6oIp7c;NXUcsqs8zW)#}YBVo%jd5*B)0Xt*BHtkUGo zIg2BC$3E^Ftba1^SjB?vWtHZQmA)905g)Q4}-npy0cmLhV;gO>cTy9dD zw{DBJMo*Z!Vf*G8({QIWFK^hoY0pk9nz}zXxnO!`&N9Yrtzbl&u`6=9sclF5jUB2g z1g*^Rl7|!JcR$I)2Wy^@!q$mcHmdb;8neh80nCF;9Q=E_HH2*gv8i5se3*=(~sdPIz;gYm6@1h8wKtFtzeWO6^Q(ST}u# zcak-Zb*kzV8*V_?`5xNab~LGJbwXBtA!ilEv=>aD9t=GyB1KP*Cl85-6Hne2Fr|mZ z4INJ=L_I=ZAQgIH2=0f|;KB+#248?@;9Kwt{0Lr!-@qT?ZFmO}H(~>B!bWVtcHD_w zxDWSZKi-Kc9L3`}hH0F^IW+MsdbotkxPlMi!}th3ijU#r_ym3lpT;laGx#jNgx|!M z@!R-4yo5i&pW-j^4SW-Shrh?a;6L!6_^wnhZI{}l9a5*%C3Qo}w z3$XJcMi-;WfUXM4j}E#q2*LUpgy4mEiUW9CETGANt_sRkNr$Tv6{;A77%0@KJ;ua| zCIh-Es89zK>VUz_7(^(@XD7WF7Y9rVTD#XQP_w{V3v8yQ^|AlF!2Z$;SI7Px_ue;h z_{c{G`@gn@qy3iF8@9)8x$XA7{Raois93=05^}D@;~a!nL3a+qIYg3!CMB(oqwX$Yj_J7q%eN_|k>bt_emC>uq!qq|3@NI{W!HQLeJ7om7S z*$`B__fr)`C3@iQ2t@`;T~NI#NmbyLj*g*W8s?HB|F>ZGYw!mA0p5al33hM6ZPE?Mg|7=fjE= zw0IOzchlVg3yL(4Nt_K{wZ6pGRH@$tI_b@mjswf2KMQkop9hUNBdkrq0l?E0>)b># zOSA%m=FiiNYz?6q`HCxb8fj!Un@2y5^p>HAETUJh6CVxD)Ie);NuKSC<_k%`6OxX= z7z~sBhL{DfIew)b6j-G3O+LO&E9|BPeBB^#3Y`t)q1ER}mF#umF;#mCc}HF8 z%TRrual2HWPG!04#3tqanE8d^RK%Z^PQ~9UO6gqW`CkF4np>J%301YV?$~-`M^@RE z)AHwLU3T74j@EHgR*~m$tCPVM>D{d%g)u`vb){*1d7ji;t~BsL?{y8so}P70ee}#3 z&+s!9{M4F^@ze}YMf*l!#6II(GhJPkfX+;5uB+LzMr!#a&$5o%DaZGnLOJEkr%cal zc}f@UOTSJ)Q0)fDKI@fku#V>c z+cEkQeO_HzW@9U|8U;^1#_?T_5c6? diff --git a/course-book/contents/.DS_Store b/course-book/contents/.DS_Store index e8c99db57feab5bc20ea716ff3b1475be805d3c9..0774f8b6f1d0658edcf984779ed485da49c9ae82 100644 GIT binary patch delta 56 zcmZp1XmOa}&nUYwU^hRb>}DQ;E+!5mLo*!(V*}I8^MqbAZhk4!#JI8S594Nbi61PR L<3)LZyq}B!02UJ% delta 198 zcmZp1XmOa}¥U^hRb!e$_{9xHUUnGZs$~r~>c^ERb diff --git a/course-book/contents/advance/smart-pointer/box.md b/course-book/contents/advance/smart-pointer/box.md index a738a676..140be256 100644 --- a/course-book/contents/advance/smart-pointer/box.md +++ b/course-book/contents/advance/smart-pointer/box.md @@ -6,7 +6,7 @@ ## Rust中的堆栈 高级语言Python/Java等往往会弱化堆栈的概念,但是要用好C/C++/Rust,就必须对堆栈有深入的了解,原因是两者的内存管理方式不同: 前者有GC垃圾回收机制, 因此无需你去关心内存的细节。 -栈内存从高位地址向下增长,且栈内存是连续分配的,一般来说**操作系统对栈内存的大小都有限制**,因此C语言中无法创建任意长度的数组。在Rust中, `main`线程的[栈大小是`8MB`](https://zhuanlan.zhihu.com/p/446039229),普通线程是`2MB`,然后在函数调用时会在其中创建一个临时栈空间,调用结束后Rust会让这个栈空间里的对象自动进入`Drop`流程,最后栈顶指针自动移动到上一个调用栈顶,无需程序员手动干预,因而栈内存申请和释放是非常高效的。 +栈内存从高位地址向下增长,且栈内存是连续分配的,一般来说**操作系统对栈内存的大小都有限制**,因此C语言中无法创建任意长度的数组。在Rust中, `main`线程的[栈大小是`8MB`](https://zhuanlan.zhihu.com/p/446039229),普通线程是`2MB`,在函数调用时会在其中创建一个临时栈空间,调用结束后Rust会让这个栈空间里的对象自动进入`Drop`流程,最后栈顶指针自动移动到上一个调用栈顶,无需程序员手动干预,因而栈内存申请和释放是非常高效的。 与栈相反,堆上内存则是从低位地址向上增长,**堆内存通常只受物理内存限制**,而且通常是不连续的, 因此从性能的角度看,栈往往比对堆更高。 @@ -44,7 +44,7 @@ fn foo(x: &str) -> String { 以上场景,我们在本章将一一讲解,后面车速较快,请系好安全带。 -## 使用`Box`将数据存储在堆上 +#### 使用`Box`将数据存储在堆上 如果一个变量拥有一个数值`let a = 3`, 那变量`a`必然是存储在栈上的,那如果我们想要`a`的值存储在堆上就需要使用`Boxt`: ```rust fn main() { @@ -64,5 +64,193 @@ fn main() { 以上的例子在实际代码中其实很少会存在,因为将一个简单的值分配到堆上并没有太大的意义。将其分配在栈上,由于寄存器、CPU缓存的原因,它的性能将更好,而且代码可读性也更好。 +#### 避免栈上数据的拷贝 +当栈上数据转移所有权时,实际上是把数据拷贝了一份,最终新旧变量各自拥有不同的数据,因此所有权并未转移。 -## Box::leak \ No newline at end of file +而堆上则不然,底层数据并不会被拷贝,转移所有权仅仅是复制一份栈中的指针,再将新的指针赋予新的变量,然后让拥有旧指针的变量失效,最终完成了所有权的转移: +```rust +fn main() { + // 在栈上创建一个长度为1000的数组 + let arr = [0;1000]; + // 将arr所有权转移arr1,由于`arr`分配在栈上,因此这里实际上是直接重新深拷贝了一份数据 + let arr1 = arr; + + // arr和arr1都拥有各自的栈上数组,因此不会报错 + println!("{:?}",arr.len()); + println!("{:?}",arr1.len()); + + // 在堆上创建一个长度为1000的数组,然后使用一个智能指针指向它 + let arr = Box::new([0;1000]); + // 将堆上数组的所有权转移给arr1, 由于数据在堆上,因此仅仅拷贝了智能指针的结构体,底层数据并没有被拷贝 + // 所有权顺利转移给arr1,arr不再拥有所有权 + let arr1 = arr; + println!("{:?}",arr1.len()); + // 由于arr不再拥有底层数组的所有权,因此下面代码将报错 + // println!("{:?}",arr.len()); +} +``` + +从以上代码,可以清晰看出大块的数据为何应该放入堆中,此时`Box`就成为了我们最好的帮手. + +#### 将动态大小类型变为Sized固定大小类型 +Rust需要在编译时知道类型占用多少空间, 如果一种类型在编译时无法知道具体的大小,那么被称为动态大小类型DST。 + +其中一种无法在编译时知道大小的类型是**递归类型**:在类型定义中又使用到了自身,或者说该类型的值的一部分可以是相同类型的其它值,这种值的嵌套理论上可以无限进行下去,所以Rust不知道递归类型需要多少空间: +```rust +enum List { + Cons(i32, List), + Nil, +} +``` + +以上就是函数式语言中常见的`Cons List`,它的每个节点包含一个`i32`值,还包含了一个新的`List`,因此这种嵌套可以无限进行下去,然后Rust认为该类型是一个DST类型,并给予报错: +```console +error[E0072]: recursive type `List` has infinite size //递归类型`List`拥有无限长的大小 + --> src/main.rs:3:1 + | +3 | enum List { + | ^^^^^^^^^ recursive type has infinite size +4 | Cons(i32, List), + | ---- recursive without indirection +``` + +此时若想解决这个问题,就可以使用我们的`Boxt`: +```rust +enum List { + Cons(i32, Box), + Nil, +} +``` + +只需要将`List`存储到堆上,然后使用一个智能指针指向它,即可完成从DST到Sized类型(固定大小类型)的华丽转变. + +#### 特征对象 +在Rust中,想实现不同类型组成的数组只有两个办法:枚举和特征对象,前者限制较多,因此后者往往是最常用的解决办法。 + +```rust +trait Draw { + fn draw(&self); +} + +struct Button { + id: u32 +} +impl Draw for Button { + fn draw(&self) { + println!("这是屏幕上第{}号按钮",self.id) + } +} + +struct Select { + id: u32 +} + +impl Draw for Select { + fn draw(&self) { + println!("这个选择框贼难用{}",self.id) + } +} + +fn main() { + let elems: Vec> = vec![ + Box::new(Button{id: 1}), + Box::new(Select{id: 2}) + ]; + + for e in elems { + e.draw() + } +} +``` + +以上代码将不同类型的`Button`和`Select`包装成`Draw`特征的特征对象,放入一个数组中,`Box`就是特征对象。 + +其实,特征也是DST类型,而特征对象在做的也是将DST类型转换为固定大小类型。 + +## Box内存布局 +先来看看`Vec`的内存布局: +```rust +(stack) (heap) +┌──────┐ ┌───┐ +│ vec1 │──→│ 1 │ +└──────┘ ├───┤ + │ 2 │ + ├───┤ + │ 3 │ + ├───┤ + │ 4 │ + └───┘ +``` + +之前提到过`Vec`和`String`都是智能指针,从上图可以看出,该智能指针存储在栈中,然后指向堆上的数组数据。 + +那如果数组中每个元素都是一个`Box`对象呢?来看看`Vec>`的内存布局: +```rust +(stack) (heap) ┌───┐ +┌──────┐ ┌───┐ ┌─→│ 1 │ +│ vec2 │──→│B1 │─┘ └───┘ +└──────┘ ├───┤ ┌───┐ + │B2 │───→│ 2 │ + ├───┤ └───┘ + │B3 │─┐ ┌───┐ + ├───┤ └─→│ 3 │ + │B4 │─┐ └───┘ + └───┘ │ ┌───┐ + └─→│ 4 │ + └───┘ +``` + +上面的`B1`代表被`Box`分配到堆上的值`1`。 + +可以看出智能指针`vec2`依然是存储在栈上,然后指针指向一个堆上的数组,该数组中每个元素都是一个`Box`智能指针,最终`Box`智能指针又指向了存储在堆上的实际值。 + +因此当我们从数组中取出某个元素时,取到的是对应的智能指针`Box`,需要对该智能指针进行解引用,才能取出最终的值: +```rust +fn main() { + let arr = vec![Box::new(1), Box::new(2)]; + let (first,second) = (&arr[0],&arr[1]); + let sum = **first + **second; +} +``` + +以上代码有几个值得注意的点: + +- 使用`&`借用数组中的元素,否则会报所有权错误 +- 表达式不能隐式的解引用,因此必须使用`**`做两次解引用,第一次将`&Box`类型转成`Box`,第二次将`Box`转成`i32` + + +## Box::leak +`Box`中还提供了一个非常有用的关联函数:`Box::leak`,它可以消费掉`Box`并且强制目标值从内存中泄漏,读者可能会觉得,这有啥用啊? + +其实还真有点用,例如,你可以把一个`String`类型,变成一个`'static`生命周期的`&str`类型: +```rust +fn main() { + let s = gen_static_str(); + println!("{}",s); +} + +fn gen_static_str() -> &'static str{ + let mut s = String::new(); + s.push_str("hello, world"); + + Box::leak(s.into_boxed_str()) +} +``` + +在之前的代码中,如果`String`创建于函数中,那么返回它的唯一方法就是转移所有权给调用者`fn move_str() -> String`,而通过`Box::leak`我们不仅返回了一个`&str`字符串切片,它还是`'static`类型的! + +要知道真正具有`'static`生命周期的往往都是编译期就创建的值,例如`let v = "hello,world"`, 这里`v`是直接打包到二进制可执行文件中的,因此该字符串具有`'static`生命周期,再比如`const`常量。 + +又有读者要问了,我还可以手动为变量标注`'static`啊。其实你标注的`'static`只是用来忽悠编译器的,但是超出作用域,一样被释放回收。而使用`Box::leak`就可以将一个运行期的值转为`'static`。 + +#### 使用场景 +光看上面的描述,大家可能还是云里雾里、一头雾水。 + +那么我说一个简单的场景,**你需要一个在运行期初始化的值,但是可以全局有效,也就是和整个程序活得一样久**, 那么久可以使用`Box::leak`,例如有一个存储配置的结构体实例,它是在运行期动态插入内容,那么就可以将其转为全局有效,虽然`Rc/Arc`也可以实现此功能,但是`Box::leak`是性能最高的. + +## 总结 +`Box`背后式调用`jemalloc`来做内存管理,所以堆上的空间无需我们的手动管理。与此类似,带GC的语言中的对象也是借助于box概念来实现的,一切皆对象 = 一切皆box, 只不过我们无需自己去box罢了。 + +其实很多时候,编译器的鞭笞可以助我们更快的成长,例如所有权规则里的借用、move、生命周期就是编译器在教我们做人,哦不是,是教我们深刻理解堆栈、内存布局、作用域等你在其它GC语言无需去关注的东西。刚开始是很痛苦,但是一旦熟悉了这套规则,写代码的效率和代码本身的质量将飞速上升,直到你用Java开发的效率写出Java代码不可企及的性能和安全性,最终Rust语言所谓的开发效率低、心智负担高,对你来说终究不是个事。 + +因此, 不要怪Rust,**它只是在帮我们成为那个更好的程序员,而这些苦难终究成为我们走向优秀的垫脚石**,