昨天我们知道了如何发布一个库等
今天继续往下学
smart pointers
)突然又多了个概念--智能指针.
相信大家对指针都很了解了,存储一个地址,这个地址指向内存里的某个地方,那个地方存储着想要的数据.
我们之前学的引用(reference
)就是这样, 引用算是指针的一种。
引用仅仅只是存储一个地址指向内存里的某个地址,没了,没有别的能力和消耗.
智能指针是另一个概念,而这个概念源于C++
,在其他语言中也存在这一概念.
由于rust
特有的所有权(ownership
)和借用(borrow
),这个智能指针又和别的语言有所不同.
rust
开发团队对智能指针的描述是: 大多数时候不仅仅只是拥有数据的地址,也会拥有这个数据.
其实我们之前就有接触到智能指针,比如String
和Vec
,它们都可以算作是智能指针.
因为它们都拥有着某块内存并且允许修改.
它们同样拥有元数据(metadata
)、额外的能力(extra capabilities
)或者担保(guarantees
).
比如String
,把它的容量(capacity
)作为它的元数据,还有额外的能力确保它的数据都是符合UTF-8
要求的.
大多数时候智能指针都是使用struct
来实现, 不过和一般的结构体不同的是,智能指针实现了Deref
和Drop
两个traits
.
Deref
这个trait
允许智能指针的实例表现的和引用一样, 所以你可以把你的智能指针当作一个引用来使用.
Drop
这个trait
允许你自定义智能指针的实例超出作用域之后做的事情.
我们之后将会分析为什么一个智能指针需要实现这两个trait
.
另外由于智能指针模式(pattern
) 在rust
中算是通用的设计模式(general design pattern
),所以这章并不会也不能描述完所有的智能指针.
不过我们将接触到标准库中最常用的那几个
Box
: 将值分配到heap
也就是堆上Rc
: 一个引用计数类型(reference counting type
), 允许多个所有权(multiple ownership
)Ref
以及RefMut
,通过RefCell
来访问(accessed
)它们RefCell
: 强制借用(borrowing
)运行在runtime
,而不是原来的compile time
也就是编译阶段.除此另外
还有*interior mutability
,*也就是内部可变性模式(pattern
): 一个不可变类型,通过暴露一个api
来允许修改内部的数据;
以及*reference cycles
,*也就是引用循环: 它们是如何泄漏内存的,要如何防止这些情况.
Box
指向heap
上的数据一个Box
类型的box
就是一个最直接的智能指针,它允许你把数据存储到heap
上,而非stack
.而存储在stack
上的是指向这个数据的指针.
有点拗口,我们梳理下:
原本数据放在stack
上,现在通过Box
转到heap
上,而原本stack
上的则变成了一个指向这个堆内存的指针.
boxes
除了把数据放到heap
上的开销之外,不会再有其余的性能开销. 不过它们也没有其它的能力(capabilities
)了,它们大多都用于一下几个场景:
context
)又要求数据是需要明确占用空间的.transfer
)所有权(请确保这个操作不会导致数据被复制一份).trait
即可.第一个场景我们等会会遇到.
第二个场景: 如果一大块数据从stack
上转交所有权,那么就需要触发copy
的trait
,这将会耗费非常多的时间,所以我们可以通过box
将这一大块数据放到heap
上,仅保留少部分引用在stack
上,这样复制起来就很快,因为只是复制指针.
第三个场景: 在rust
中一般被叫做*trait object*
trait
对象,我们将在第十七章接触到[3].
Box
将数据放到heap
上直接来看下例子
Box
也是prelude
的,所以也可以直接用不需要手动导入.
我们把5
这个原本应该存储到stack
上的标量类型,现在存储到了heap
上.
现在b
是一个Box
的类型,它是一个智能指针,指向存储堆内存里5
所在的地址.
b
这个box
在超出作用域之后也是正常失效被释放,包括它指向的内存空间.
不过一般我们不会把单个值比如5
这种标量类型放到heap
上,正常情况下放stack
上才是恰当的.
Box
让递归类型能被使用这个场景就是场景1
递归这玩意儿通俗点的说就是套娃.
我来画张图
有点丑,但是应该挺好理解,一个元组的第二个数据的类型和这个元组的类型是一样的.
这种数据类型在编译阶段是没办法通过的,为什么呢?
因为理论上你这个套娃能套到内存溢出.
这个时候就轮到我们的Box
出场了,box
是可知大小的, 毕竟已经变成了一个指针.
这样处理其实只是把问题从编译阶段迁移到runtime
阶段,绕过编译无法通过的问题而已.
这种套娃类型在其它函数式语言中一般叫做cons list
.它有两种值,一个就是和它自己类型一样的,另一个就是Nil
,表示到底了.
需要注意Nil
和None
不是同一个概念,None
是无效不存在的意思,而Nil
表示的是到底了的.
大部分情况下我们都不会在rust
中使用这个类型,一般vector
就够用了.
我们来看下rust
中怎么使用它
我们定义了一个枚举List
,它有两个变体,一个是Cons(i32, List)
,接收的数据一个元组类型;另一个则是Nil
.
然后我们来运行下
直接就没法通过: 这是一个infinite size
的类型.
在处理这个问题之前,我们来简单的认识下rust
的编译器是怎么判断一个类型需要多少空间的.
先来看下例子
这个Message
的例子在之前我们接触过.
在编译阶段,编译器会把这个Message
的枚举里的变体都过一遍,发现Quit
压根不需要任何的空间,而Move
则需要一个至少能容纳x, y
两个数据的空间,其它也会看一遍需要多少.
由于一个枚举一次只会使用一个变体,比如Message::Move
这样,所以rust
的编译器就以这四个变体中所需要空间最多的那个为标准存储Message
的类型数据.
然后它遇到了List
这个套娃类型,这个时候就没办法推测了
现在我们知道了编译器是如何推断空间以及为什么推断不出来套娃类型需要的空间的原因了.
回到我们的代码中
这个时候就得借助Box
把这个问题移到runtime
来处理,runtime
时根据实际情况有内存分配器分配空间.
前面说了Box
是一个智能指针,所以它的空间肯定是可知的,毕竟到死都只是个指针(古惑仔不用脑,一辈子都是飞机~).
我们来改下我们的代码
这回没报错了.
通过Box
这么一包裹,这里就只剩下一个指针
需要注意的一点是:它在超出作用域的时候就会被释放,所以目前暂时不用担心内存泄漏的问题.
Deref trait
把智能指针当作引用(reference
)来对待实现Deref
这个trait
之后,我们就能自定义解引用了(dereference
)的操作符*
了代表的行为了.(这个不是相乘的意思).
同时实现Deref
这个trait
之后就能把智能指针当作普通的引用来使用了,当然,它依旧是一个智能指针,你也可以把它当作一个智能指针来使用.
我们先来看下*
操作符
我们先来看个例子
这样是会报错的,
为什么呢?
因为assert_eq!
这个宏的right
是一个不可变引用,它的值是一段指向内存的地址,而left
则是一个整数.
我们再改下代码
这样就正常了.
通过这个例子,我们可以得出*
操作符是通知编译器去跟着地址找到内存里的数据然后读取数据,所以这里实际上就类似于
Box
当成引用来用我们来改下上面的代码
可以看到y
和z
在这里的表现是一样的,去掉*
也是有问题.
需要注意的是和引用不同的是Box
是复制了一份x
到heap
上,然后再用指针指向这个heap
上的数据,而不是直接创建一个引用。
我们来自己搞一个类似Box
的模拟下内部原理。
Box
底层类似一个元组结构体,所以我们也搞一个,它还有一个new
的关联函数。
所以我们先搞一个架子。
我们这个MyBox
元组结构体接受一个数据,泛型T
表示想要接收所有类型的值。
而元组数据是存储在stack
上的,我们目前暂时没有办法去模拟放到堆内存上,所以将就下,毕竟我们的重点是这内部是怎么运作的。
现在我们可以使用这个MyBox
了,你能将它类似引用的方式来使用,
不过编译是会报错的,因为我们并没有定义如何解引用。
然后为了可以使用这个解引用,我们要实现Deref
这个trait
。
实现Deref
的要求是要实现deref
这个method
,我们来写下。
由于Deref
还是比较不常用的,所以没有prelude
,我们需要手动引入。
type Target = T
,前面的文章我们有说到过,这个是关联类型,表示私有返回值的类型。
deref
这个方法返回的是我们之前接收的数据。
不过需要注意是&self.0
,而不是self.0
,所有权还得是自己保留。
现在我们可以直接使用*
操作符来解引用了。
实际上*y
操作在编译阶段会变成*(y.deref())
这样,这也算是一种语法糖。
现在我们知道了Box
内部大概是怎么样的了。
Functions
)和方法(Methods
)中隐式的强制解引用(Deref Coercions
)这个强制解引用(*Deref Coercions*
)可能我翻译的不是很对,如果觉得不对可以点一下注释去官网看。
这个强制解引用可以让一个实现了Deref
这个trait
的类型的引用转换成另一个类型的引用。
有点拗口,举个栗子:&String
可以变成&str
,因为String
类型实现了Deref
这个trait
。
在rust
中,它对于函数和方法的参数来说是非常方便的,当然,前提是这个参数得实现Deref
这个trait
。
这个过程是自动的,当你传入的参数类型和要求的参数类型不一致的时候就会自动通过一系列对deref
方法的调把参数类型转换成函数需要的类型。
可以看到完全没问题,这也就是为什么之前我们推荐&str
作为参数类型的原因。
当然,我们也可以用我们的MyBox
作为例子
可以看到也没有问题,因为也实现了Deref
这个trait
。
编译器在发现类型不对的时候就会调用deref
这个方法,然后获得到&String
类型的数据,然后String
自身又是实现了Deref
这个trait
的,所以它的deref
方法也被调用了,然后就变成&str
类型了。
这也就是为什么会用:a sequence
也就是一些列这个词来描述的原因了。
以上的行为都是发生在编译阶段的,所以不用担心有runtime
的损耗。
我们前面看到的都是不可变的,如果用在可变的话就会报错
比如:
这里编译器有提示我们实现DerefMut
,而非Deref
,我们来改下。
现在就没有报错了,类型也是正常自动转换。
目前我们还不知道如何改变&str
的数据,所以我并没有改变hello
方法中的name
数据。
这一块可能有些乱,稍微梳理下:
我们这个MyBox
实现的DerefMut
的deref_mut
方法返回的是我们存储的值,所以在发现这个数据类型对不上的时候它会变成&mut String
,然后String
它自己又实现了DerefMut
这个trait
,所以它也能变成&mut str
类型。
以下几种场景是会触发强制解引用的
T
实现了Deref
的前提下,&T
变成&U
是没有问题的,比如上面的&String
变成&str
,就是因为String
实现deref
方法时的Target
是&str
。T
和U
都是mut
的,并且T
还实现了DerefMut
,比如&mut String
变成&mut str
。T
是mut
的,这样也是可以转的,比如&mut String
变成&str
。这里需要注意的是从mut
变成immut
是可以的,但是从immut
变成mut
就不行,原因还是因为借用规则和内存安全。
其实如果从头到尾只有这一个引用,那么它从不可变变成可变的也没啥问题。但是rust
的借用规则无法保证这一点,所以直接封杀了事。
Drop Trait
来清理运行中的代码这个trait
其实我们之前就已经接触过了,一般一个变量超出作用域的时候就会调用这个trait
的方法释放自己占用的资源。
既然是用来释放资源的,那就意味着所有的类型理论上都可以实现这个trait
。
至于为啥要放在smart pointer
这一章里讲,那是因为智能指针在实现的过程中大多数时候都是需要实现这个trait
的,所以绕不过就干脆放这里讲了。
我们可以通过实现Drop Trait
来自定义一个数据超出作用域的时候要做什么。
实现Drop
这个trait
的要求是实现drop
这个方法。
我们来试下,Drop
相对来说用的是比较多的,所以它被prelude
了,我们不用手动引入。
可以看到是created
的打印先触发,而drop
里的打印是在c
和d
超出作用域之后才触发的。
不过你应该注意到了d
的drop
比c
的触发早,因为变量是倒序的方式释放的。
有一点需要注意,那就是不用担心我们实现的drop
覆盖了原本释放数据的drop
,实际上我们的代码是合并到原本释放数据的代码里面去的。
std::mem::drop
提前释放资源我们并不能禁止drop
方法和调用drop
方法来延迟或者提前释放资源。
延迟官方没有给api
,但是提前有:std::mem::drop
。
我们直接来看下例子
可以看到drop
的打印提前了。
你可以基于Drop
和这个std::mem::drop
自己搞一个内存释放器,也不用担心会和系统自带的冲突。
绝大部分情况下我们的数据都只有一个拥有者。
还有极少部分情况存在多个拥有者,比如在图结构里,一个节点可能有多个点指向它,那么这个节点理论上就存在多个拥有者。
那么什么时候才能释放这个节点的数据呢?自然就是没有点指向它的时候,这个时候就轮到计数智能指针登场了。
Rc
是reference counting
的意思。
那么什么时候该用这个trait
呢?
当你不确定某个heap
上的数据在什么地方,在第几个引用结束的时候才清空,那么你就可以用这个Rc
,等到没有引用的时候让它自动被释放。
Rc
分享数据我们先来看个例子
上面的代码画成图类似下面这样
不过现在的代码是没办法通过编译的,为什么呢?因为a
的所有权交给了b
的元组第二个参数,c
自然就拿不到了。
我们可以改为传入引用来避开这个问题
是不是很乱,看的头都炸了。
这个时候我们就可以用Rc
来代替Box
。
直接就比生命周期看起来清晰很多。
Rc
也是没有prelude
,需要手动引入的。
Rc::clone
并不是和之前用过的clone
方法一样复制一份数据,而是增加这个引用的计数。
其实这里可以用a.clone()
,但是rust
开发团队是习惯是用Rc::clone(&a)
,两个都是一个意思。
clone
一个Rc
来增加它的计数我们改下上面的例子来看下这个计数的过程
可以用Rc::strong_count()
来获取这个数据的引用计数。
可以看到当c
超出作用域被释放之后计数就减少了1
。
当main
函数退出调用栈之后计数就会变成0
,然后指向的内存的数据就会被释放。
Rc
使得一个数据可以有多个“拥有者“,在某些时候是挺有用的,另外也不用担心会有泄漏的问题,为0
的时候就自动释放了。
最后提一嘴,这个方法只能用于单线程。
interior mutability pattern
)*Interior mutability
*是rust
中的一种设计模式,它允许我们在数据存在不可变引用的前提下修改数据。
正常情况下是不能这么做的,因为rust
的借用规则。
但是我们可以告知编译器这段代码由我们自己来判断是否正常没问题,这样虽然是unsafe
的,但是确实能通过编译。
而Refcell
就是一种基于内部可变性模式的类型。
Refcell
强制借用规则在runtime
时生效前面说了,我们通过这种方式把借用规则生效的时间迁移到了runtime
,而非在编译阶段就判断。这也就意味着这段代码是unsafe
的,一旦出了问题就是panic
。
不过小意思,js
中见得多了~
不过能放到编译的一定放编译阶段,runtime
出问题的成本太高了,一不小心就是补上线。。。
来总结下Box、Rc、Refcell
这三个分别是干啥的和区别,在什么时候用(突然莫名其妙的总结。。。估计是怕后面混了)
Rc
,一个计数智能指针,允许一个数据存在多个"所有者"。Box
和Refcell
则只能用于单个“所有者”的场景。Box
会将数据迁移到heap
上并创建一个指针存放这个数据的地址。 它的借用规则和Rc
一样都是在编译阶段就判断的,只是Box
能绕开一些编译阶段无法处理的问题。而Refcell
则是把借用规则的判断放到了runtime
阶段来绕开编译会报错的问题。Refcell
则是可以强制借用规则判断放到runtime
阶段执行。你可以在Refcell
里面改变数据,即使这个数据是不可变的,因为你已经通知编译器不要管这块代码了。我们来看个场景
interior mutability
):一个对不可变数据的可变借用先来看下平时我们可变借用一个不可变数据
很明显是会报错的
原数据都不可变,自然不会有可变借用。
对于这个问题,我们一般用struct
包裹。
比如这是个struct
里的内部数据,它有一个method
叫做set
,用来修改自己的数据,然后还有一个get
方法,返回这个数据的不可变引用。这样的场景应该挺常见的。
这里只有一个改变数据的入口:set
这个方法。
但是有时候就是不想写这么多东西,就是想要它是可变的,就是懒,怎么办呢?
这个时候就能用Refcell
,把代码放到它里面就可以直接改变数据。
不过需要注意的是,它只是把问题移动到runtime
而已,如果你的借用规则有问题,比如同时存在一个不可变借用和可变借用并且你这个可变引用改变数据是在可变引用声明之后使用之前,那大概率还是有问题会被panic
掉。
至于要怎么改。。。等会再说
Mock Objects
Mock Object
下面直接就叫模拟对象了。
有时候我们测试一块代码会用某个类型代替另一个类型,这么做是为了观察代码的反馈以及判断这块代码是否正常。
这种占位类型(placeholder type
)就叫test double
,这下面直接就叫测试替身了。
而模拟对象则是测试替身中的其中一种(白金之星~),它会记录测试过程中发生的事情。
rust
中没有object
这种类型,也没有在标准库中提供mock object
。不过好在我们有struct
,可以基于struct
自己写一个~
我们来写一个模拟对象,一个小跟踪工具的demo
,用来跟踪计算某个数和当前最大值的差值并给出提示信息。
这个小工具有两个method
,一个是new
,这个自然不必多说。还有一个set_value
,它有两个参数,一个自然是self
,不过注意它是mut
的,然后第二个参数是需要计算的值。
目前这只是一个demo
,实际上我们可以拓展为跟踪不同场景,然后发送邮件等。
使用这个跟踪工具库的开发者只需要传入实现了Messenger
这个trait
的参数,就可以使用了,不需要知道内部是如何实现的,当然,api
名字还是得要知道的。
在实现send
这个method
的时候,我们可以自定义如何发送数据或者对提示信息做什么处理。
send
方法接收两个参数,第一个自然是self
,注意是不可变的,毕竟是暴露给库里的方法,还是不要给所有权最好。第二个参数则是我们的提示信息的切片。
为什么要单独抽出来这个Messenger
的trait
呢?
这么做是为了模拟的数据也可以实现这个Messenger
从而调用这个库,不然就比较麻烦了,需要真实数据类型/真实场景才能开发。
我们目前唯一可以改变数据的入口是set_value
,可以获取信息的入口是send
这个method
。
然后我们来写一个测试用例,测试返回的信息是否是我们想要的。
我们创建了一个MockMessenger
的struct
,它有一个私有数据sent_messages
,是一个vector
,用来存储 返回的提示信息。
但是这里有一个问题,那就是前面说的我们的send
里的self
是一个不可变的引用,而这里我们需要把提示信息存储到sent_message
里,自然就需要self
是mut
的,这里就有问题了。
那么咋办呢?
这是你应该已经发现了,这个问题就是上一个小章节里提到的那个问题。它暴露了一个不可变self
的方法,但是我们又想要是可变的。
这时就轮到这一章节的主角登场了。
我们来改下
这里面一共调整了四处地方
struct
的时候我们使用Refcell
包裹Vec
new
方法创建实例的时候,我们使用Refcell
包裹vec![]
sent_messages
这个Refcell
实例的时候,我们调用了borrow_mut
这个方法assert_eq!
比较长度的时候,我们调用了borrow
这个方法borrow_mut
方法返回一个可变引用,源码咱就不看了,现在也看不懂。
那borrow
自然就是返回一个不可变引用。
这样就能通过编译测试了
我们来看下它做了什么
Refcell
在runtime
跟踪借用我们之前创建引用的时候使用的方式是&
或者&mut
,现在我们多了一种方式。
通过Refcell
实例调用borrow_mut
和borrow
。它们对于Refcell
来说是相对安全的。
borrow
返回的实际上引用是Ref
类型,而borrow_mut
自然就是RefMut
,都是智能指针。
不过它们都实现了Deref
这个trait
,所以自然可以被当做引用。
Refcell
会监听所有的Ref
和RefMut
,所以能知道当前有多少个智能指针是有效的。
每次调用borrow
方法,它就会给Ref
的智能指针计数加1
,同理调用borrow_mut
会给RefMut
的计数加1
。
而当这些引用超出作用域的时候就会减1
。
不过上面都是发生在runtime
期间的。
正如前面说过的,如果这个时候我们违背了借用规则,就会直接panic
。
来看个例子
其实还是之前的例子,不过我们在实现send
的时候我们同时声明了两个可变引用,这时违背借用规则的。
你可以把这段代码放到main
里面使用,然后cargo debug
试下,编译是没有问题的。
但是当你cargo run
或者cargo test
的时候就会报错了。
Rc
和Refcell
搭配前面我们说了Rc
是一个引用计数智能指针,然后上面一个小章节又说到我们在调用Refcell
的borrow
和borrow_mut
的时候也会对引用计数。
并且同样在引用创建或者超出作用域的时候增减对应的数量。
目前我们已知的不同点:
Rc
还是发生在编译阶段,而Refcell
则是发生在runtime
阶段。Rc
只能返回不可变引用,而Refcell
可以返回可变引用。Rc
允许多个引用,而Refcell
支持多个不可变引用或者单个可变引用(借用规则)关于第二第三点,我们有一个想法,那就是结合Rc
和Refcell
,创建多个可变引用?
我们直接借用之前的那个cons llist
吧,虽然有点绕。之前我们用Rc
改写了之后,可以同时存在多个引用,在不需要使用生命周期的情况。
但是我们并不能去改数据,因为是不可变引用。
然后我们用Refcell
来包裹,让它变成可变的
我们用Refcell
包裹了5
创建了一个Refcell
的实例,然后又用Rc
包裹了这个实例,这样就允许存在多个引用。
然后我们又调用Refcell
的borrow_mut
方法创建一个实现了RefMut
的可变的引用,这样就存在多个可变引用了。
不用担心这样有问题,毕竟借用规则还是在的,只是跑到runtime
去了。
不过记住目前哦们学的只有单线程的。
内存泄漏这玩意儿难搞,没有GC
的语言就不多说了,而有GC
的语言也没办法保证能解决内存泄漏的问题。
rust
虽然有自己独特的所有权规则,但是也是有可能遇到内存泄漏的问题的。
不过rust
开发团队把这个当做是rust
的一种安全的场景。。。。。。这里咱就不吐槽了,毕竟确实难解决。
还是之前cons list
的例子
我们还写了一个tail
方法,判断是否还没到底。
然后我们来调用下
重点是在*link.borrow_mut = Rc::clone(&b)
也就是说a
又引用了b
,而之前b
引用了a
。
这个时候就有问题了,a
和b
的引用不会变为0
。
好像以前IE
的垃圾回收器用的标记计数法也有这个问题(八股文走一波~)。
直接官网偷的图,这样方便理解。
我们还有一句println!
注释掉了,如果注释回来就会一直打印直到溢出,因为这个时候已经循环引用了。
这场景正常情况下我们是不会遇到的,不过也不好说,一些递归场景还是有可能遇到的。
怎么说呢,自己多注意吧~
Weak
代替Rc
来避免循环引用相对于Rc::clone
创建的强引用计数智能指针,Rc::downgrade
会创建一个弱引用(weak reference
),也就是Weak
类型的智能指针,这个弱引用和强引用的区别在于分享所有权。
前面我说Rc
是在创建多个引用,其实不是很对。因为这些引用实际上身份和原数据拥有者是一样的。
而通过clone
出来的强引用可以通过strong_count
来查看有多少“拥有者”。而弱引用不会被统计在strong_count
里。它实际上不是这个数据的拥有者,也就是说它并不会影响Rc
实例的清除。
而强引用被清空后也会让弱引用被清空,所以我们实际上可以用弱引用来回避循环引用的问题。
不过由于弱引用会因为强引用被清空而失效,所以开发者需要注意使用的范围。可以用过调用Weak
实例的upgrade
方法,这个方法会返回一个Option>
,可以用这个来判断是否还有效。
让我们来试下。
其实套娃类型Cons List
最常见的还是树结构,子节点类型和父节点类型是一样的。
我们直接来写一个
有一个叶节点和枝干节点,叶节点是枝干接点的子节点。
我们用Refcell
包裹这个children
,这样就能拓展了。
不过目前leaf
和branch
之间的关系只有开发者自己知道,而leaf
并不知道branch
是它的父节点。
我们来改下代码,让这个leaf
节点知道它的父节点是这个branch
。
不过在这之前,我们需要知道这个父节点得是什么类型的。如果按照之前的写法,这个叶节点就会引用这个枝干节点,那么就会形成前面说的循环引用问题。
这个时候就轮到Weak
出场了。
父节点失效之后子节点自然得失效,而相反子节点失效后父节点不一定失效,用Weak
太合适了。
现在我们的子节点引用了它的父节点,但是没有这个父节点的所有权。
ok
,类型我们解决了,我们来使用下
可以看到parent
这个字段的数据不会展示所有。
可以看到并没有循环引用。
然后我们用Rc::strong_count
和Rc::wak_count
来看下计数
这里把leaf
的存放到父节点以及弱引用父节点都放到了作用域里面。
可以看到在超出作用域之后weak
变成0
了,同时strong
也变回了1
,并且这个时候的branch
由于超出作用域也挂了。
所以我们使用Weak
可以避免循环引用
不知道怎么说。。。。
这一章我们知道了什么是智能指针,它实现了Def
和Drop
两个trait
。
然后我们还学了智能指针中三个比较常用的类型:
Box
:能把数据迁移至heap
上并创建一个指针。Rc
: 能给一个数据创建多个“拥有者", 并记录它们Refcell
:可以在runtime
时再执行借用规则的判断,给不可变数据创建可变引用。最后我们还知道了什么情况下会存在内存泄漏的问题以及如何使用Weak
规避这个问题。
最后吐槽下。。。。这章内容太多了,想吐。早知道还是分几天来写算了。
但是下周两个需求,不太一定有时间摸鱼,烦。
另外可能有什么地方说的不对,请务必对照官网来看。。。
最后祝大家新的一年身体健康~
编辑于 2023-01-03 22:38・IP 属地广东