Rust学习 II
Rust - II
I. Intro
变量的lifetime之前的部分,理解起来都比较简单。而一到lifetime出场,一群妖魔鬼怪也就跟着出场了。不过其实是因为之前的两天学习中,对于变量的引用,所有权的租借理解不够到位。本文仍然是在跟着Rust官方(的非官方,它自己写的)教程学习过程中,整活扩展的一些记录。
II. 使得stack更强
2.1 迭代器的构造
在之前的实践中,为了查看我的stack是否正确,我使用match语法糖写了一个简单的遍历方法。但这种遍历方法不够强,它无法使得我可以直接使用for循环遍历此stack。如果要直接for循环遍历,正如C++中一样,需要重构(或者实现)iterator相关方法。这里我们介绍IntoIter
,Iter
与IterMut
1 | pub struct IntoIter<T>(List<T>) where T: Default; |
这里有两点需要注意:
- 首先,iterator这玩意就像C++中的迭代器一样,与原类型是不同的。这也就是为什么我们需要重新定义一个叫做
IntoIter
的模块。实际上这告诉我们,不是List<T>
本身有迭代器,而是其在执行into_iter
函数之后,返回的迭代器可以进行迭代。那么,以上代码的三个块,分别代表了:定义需要返回的迭代器类型(IntoIter
),给List
实现返回迭代器的操作,实际实现迭代器的内部功能next
self.0
是什么玩意?注意到,此struct只有一个域,本质上是一个tuple struct(内部的变量没有名字),那么他们的域可以按照self.k
来依顺序访问。此外,由于这里调用了pop,并且我们发现,返回值是Option<T>
,不难发现此迭代器会逐步销毁原List
。在执行into_iter
操作之后,原变量(stack本体)应该丧失了所有权,转移到一个IntoIter
变量中。我们要求原stack是mutable的(不是mutable也没有意义,全域静止管理的stack是吧),否则无法进行pop。
怎么说呢,此实现需要非常遵守固定程式,故也没有给我整活留下太大的空间。
但是很多时候,我并不想使得原来的stack被销毁,我只想迭代一下,输出看看。则我们可以实现一个返回引用的iterator,也即我希望内部的所有逻辑都是引用:
1 | pub struct Iter<T> where T: Default { |
逻辑看起来很正确,对吧?看了一眼官方教程,很不一样。。。但是没关系!要有自信。结果一编译就爆炸了,无奈瞅了一眼官方教程,结果发现它也爆炸了。可以的,我们都炸在了 lifetime 这玩意上。
2.2 Impotant: Lifetime
人的一生啊(lifetime),他自己就不知道。一个人的前途命运啊,当然要靠自我奋斗。啊同时还要考虑到历史的进程。我当年在上海当...(内容快涉嫌违规了)。lifetime,其实是我们很熟悉的概念,就是变量的生存周期嘛。但是不管是C、C++还是Python,都已经把生存周期管理的事给做了,我们其实很难接触到生存周期的问题(除了C++中,最好不要返回函数中临时变量的引用或指针这样的例子外)。正如官方教程所说:
Lifetimes are unnecessary in garbage collected languages because the garbage collector ensures that everything magically lives as long as it needs to. Most data in Rust is manually managed, so that data needs another solution. C and C++ give us a clear example what happens if you just let people take pointers to random data on the stack: pervasive unmanageable unsafety. This can be roughly separated into two classes of error:
- Holding a pointer to something that went out of scope
- Holding a pointer to something that got mutated away
Lifetimes solve both of these problems, and 99% of the time, they do this in a totally transparent way.
抄了好大一段啊,我就不翻译了,反正看不懂的都应该反思一下自己高中大学学的英语都用来干什么了,看不良网站去了?省流:Rust可以避免引用指针失效问题,各种处理都是完全透明的。强啊,只能说。
但同时lifetime是一个相对较为复杂的概念(上午看了半小时概念,愣是没看懂)。首先我们确定一下,lifetime的应用场景。对于普通的变量而言,其实一般不需要考虑lifetime概念,倒是引用(reference)这些玩意,由于他们可能指向一个已经move、out of scope或者其他原因失效的变量,我们需要特别小心。也即:对于引用而言,我们希望其指向的变量活着的时间可以至少与引用变量本身一样长。别在引用还valid的时候,白发人送黑发人了。那么为什么编译器可能会有lifetime的困惑呢?这里我引用Youtube上一个博主的例子
1 | fn main() { |
这段程序看起来很正确,但实际上没办法编译。为什么?编译器认为:longer_string
函数输入输出的lifetime是有问题的。在这个例子中,没有什么问题,但是如果main改写成:
1 | let str1 = String::from("The first string."); |
确实就是错的了。但是你会觉得,诶呀这不就跟js一样嘛,一个块内部的变量不能在外部访问,很正常啊做一个检查应该就知道。确实,但如果进一步改呢?
1 | let str2 = String::from("2nd string."); |
这里已经不仅仅是result在局部块中的问题了。可以根据longer_string
的定义知道,返回的引用是& str1
。而即便println可以访问到变量result,result指向的也已经是一个销毁后的变量。此外,上面这个例子的流程已经非常简单了,更复杂的情况下,编译器无法判定在引用result存活时,其指向的内容是否也是存活的。这就需要我们进行限定:
1 | fn longer_string<'a>(x: &'a String, y: &'a String) -> &'a String{ ... |
这里'a
表示限定的lifetime,主要用来限定输入输出的lifetime(输入之间其实无所谓)。lifetime实际上是generic的,我们需要用类似模板的方法,使用尖括号进行限定。a
实际只是一个名字,我们可以随便起,但是惯例就是单个小写的字母。注意,此处虽然x
与y
的lifetime可能不一致,我们仍然写了两个一样的'a
,这种情况下,编辑器将会选择更小的一个作为输出的lifetime。这样的lifetime限定,根本目的是:
使得输入至少可以与输出存活得一样久。
注意:
lifetime用在type
,函数、结构体、枚举类等上,一般不需要用在函数的内部。个人的理解是,这应该是一个声明的“特性”,而不需要出现在实现里。我们尝试使用lifetime来修改stack的iterator:
1 | pub struct Iter<'a, T> where T: Default{ // 这里应该表示的是,Iter能存活多久,this指向的内容就应该存活多久(因为this引用的lifetime现在与Iter一致) |
这里注意,不能写成:<'a, T: Default>
否则会出现一些'a
not bounded
这种奇怪的,我不知道如何处理的错误。关于lifetime省略(ellision)规则,可以参考官方教程An
Ok Stack:
Iter,也可以看上文提到的油管博主的视频。exmmm结果我发现,通过作者实现的方法,后面还有报错?我这一版本加上lifetime
annotation就直接过编译了。整活!开始(但实际上,我和教程实现的完全不是一个东西,虽然功能上类似,说实话,我觉得作者实现有点复杂?(开始飘了))。让我非常惊讶的是...
我感觉这玩意是个立即数啊,怎么还能引用呢?
1 | assert_eq!(iterator.next(), Some(&4)); // 写的iter测试,如果去掉&符号,就会报错,说iterator.next()返回了一个Option<&i32>,而你这是Option<i32> |
这里我保存教程中几个有趣的(颜)表情(作者编译连跪三把):
(╯°□°)╯︵ ┻━┻ ---> (ノಥ益ಥ)ノ ┻━┻ ---> 😭
看到最后,作者直接祭出了as_deref
,好吧,我不能因为我的实现可以编译就不管他这一版本,我也想知道as_deref
是干什么的。作者做的事情很简单:作者的next项保存的是node,由于node实际已经是内部Link内部的类型了(被封装了,因为Link是Option<Box<Node<T>>>
),作者需要通过map将其取出:
1 | next: self.head.map(|node| &*node) // 直接这样会导致移动的(self.head被移动),需要as_ref |
注意,node本身(由map,也即match类方法取出)是Box<Node<T>>
,需要从堆中取出则用*。作者用了deref
,但是我发现只要改成:‘
1 | pub struct Iter<'a, T> { |
并且把map中node之前的&*
去掉其实就可以了。as_deref
,官方文档的意思写的很明确:
1 pub fn as_deref(&self) -> Option<&<T as Deref>::Target>Converts from
Option<T>
(or&mut Option<T>
) toOption<&mut T::Target>
.Leaves the original
Option
in-place, creating a new one containing a mutable reference to the inner type’s [Deref::Target
] type.
可能刚开始有点晕,什么是T::Target
?结合stack例子,以及这个函数的名字就可以知道,这里实际上做了一个dereference操作,从Box
中将被Boxed的元素取出,target实际上就是Node<T>
。这里,as_deref
做了两件连续的事情:(1)给返回值增加引用,(2)dereference操作,取出box内元素。其实看std::ops::Deref
的说明也可以知道:
Used for immutable dereferencing operations, like
*v
.
那么,self.head
进行as_deref
操作的结果也就很明显了。从&Option<Box<Node<T>>>
先转化为Option<&Box<Node<T>>>
再成为Option<&Node<T>>
。我自己原来的实现,说是this: Link<T>
但由于Link<T> = Option<Box<Node<T>>>
,故实际上我原来的实现和我自己的修改next: Option<&'a Box<Node<T>>>
应该是没有太大差别的。
2.3 理解有误?
尝试实现IterMut
但出现了问题,此问题我想了很久没有想明白,于是在stackoverflow上挂了一下,两小时后有两个人回复了我。其中一人的“修改”实际上与官方教程完全重合,对我的帮助不大,但他在回答中提到的东西对我很有启发。问题在这里:Stackoverflow:
Lifetime Confusion for iterator。这里我重新列一下:
1 | pub struct IterMut<'a, T>{ |
使用如上方法实现一个IterMut
,(这里为了可以更加清晰地看出发生了什么事情,将map
替换为了match方法,原来的实现在注释中)。这里会报错:说我发返回的Some(&mut node.elem)
的声明周期实际上是'1
(与匿名生命周期引用&mut
self一致),而我在impl
块中显式定义了一个'a
,两者冲突了,错误是:
returning this value requires that
'1
must outlive'a
这里我根据歪果仁们的回答以及我自己理解,解释一下产生此问题的原因。首先,我们定义了一个IterMut
,它的生命周期是'a
。这个IterMut
使用iter_mut
函数返回,那么我们可以根据生命周期省略规律得到,IterMut
的生命周期就是List的self
周期。也即'a
与List
(stack本体)一致(或者说,IterMut
至少与stack本体活得一样久)。接下来,由于我对Rust语言的理解有限,我无法确定以下两个想法哪一个是正确的,所以我在此分支,两个都记录下来:
(1)个人更加倾向于这种解释:next函数中的&mut self
可能有单独的生命周期,因为这里的self只是一个引用,不一定需要与实例化的本体有同样的生命周期(更短即可)。也即假如IterMut
的生命周期就是'a
,&mut self
可能有一个单独的生命周期'b
。self.this没办法导出正确的生命周期到node中,毕竟self.this生命周期应该是'a
(与IterMut
一致),而对于self的引用又是生命周期'b
的。如果要保证传出的&mut node.elem
是有效的,'b
显然应该长于'a
,(因为Item = &'a mut T
',返回值的生命周期是'a
的)。而如果'b
长于'a
,会产生一个问题:我如何确定返回值何时无效呢?如果'b
很长,在下一次调用next时,会对此IterMut
创建一个新mutable
reference,而原来的mutable
reference(还记得吗,它的生命周期是'b
)还没有销毁!
Rust不允许对同一个变量保留多个valid的mutable reference,甚至以下情况也是不允许的(一个引用整体,一个引用内部域):
1 | pub struct TestStruct {a: i32, b: f32} |
关于此问题更详细的介绍,见:Multiple Mutable Reference
此外,关于这种解释,stackoverflow上的大佬也说:
- So to use the exclusive reference there must be a guarantee that you can't obtain the same mutable reference again. (Note that as written, if you call
next()
twice you'll get the same mutable reference back, which would be a problem -- that's why the lifetime of the return value must be bound to the lifetime of&mut self
in this particular implementation.)- The principal problem lies in trying to take a reference to the whole
head
. While that reference lives, you can't hand out mutable references to anything inside it.
(2)next没有单独的生命周期,其中的&mut self
生命周期也是'a
。这也会引发类似的问题:我们已经知道'a
是非常长的(与stack本体一致),而这里虽然&mut self
在next函数结束之后,看似已经不见了(本次&mut self
已经销毁了),但实际上并非如此,由于我们定义了&mut self
的生命周期是'a
,它实际还是存活着的。故也会出现多mutable
reference的问题。
已经知道错了,求求Rust不要再打我了,\(T益T)/。接下来我们来细致分析一下教程的实现。在stackoverflow的回答中,参与回答的大佬说:
The principal problem lies in trying to take a reference to the whole
head
.
原实现中,在构造IterMut
的过程中,this传入的参数是&mut self.head
。而self.head
的生命周期非常长(与stack本体一致)。如果像作者一样,this指向的不是一个Link结构,而是一个Node,可能可以解决问题。教程中实现是:
1 | IterMut { |
as_deref_mut
可以将&mut Option<Box<Node<T>>>
转化为Option<&mut Node<T>>
。而self.head.as_deref_mut()
得到的这个node,生命周期是什么呢?个人的理解是:可以很短!因为这不是stack中常驻的head(只要stack在就有),node是可以轻易invalidate的。也就是说,之所以用node写,就是因为head已经成了...
老不死的玩意。对于一个node而言,只要take它,它就之后就没了,销毁了,使得此生命周期不会影响代码的其他部分,也不会使得某个引用活太长而挤占可以用的mutable
reference资源。其他其实没有太好说的。吐槽一下,这样的next peeking...
貌似会毁掉整个stack,我不能做到修改之后还保证原来的结构?
2.4 生命周期的其他问题
lifetime深究起来很是复杂,在编写程序的过程中很容易搞糊涂。这里我想再分析一下stackoverflow上关于conflict lifetime的另一个例子。个人觉得,这个例子理解起来较为简单,并且如果你真的理解了lifetime,分析这个例子是很轻松的。原问题:Cannot infer an appropriate lifetime for autoref due to conflicting requirements
其中一个佬将问题进行了简化:
1 | struct FontLoader(String); |
试问,为什么load函数编译时,无法正确导出lifetime?首先,看第二个impl
块,此块限制了(定义了)Phi
结构体的生存时间。但此处,&mut self
却仍然是一个local的lifetime(并不一定是'window
),接下来的流程是:
- loader.load。loader没有标注生存周期。load之后,返回的load将会装入loader的生命周期(这里符合生存周期省略规则,故
&self.0
对应的生存周期 --- loader的生存周期被装入。) - 返回后,希望使用Option封装装入
self.font
,但是self.font的生命周期是'window
...,那么loader的周期是'window
吗?我们已经说了,并不见得。假如没有人操作这个变量,很可能self.loader
直到结构体Phi
被销毁时才随之销毁。但是... 假设它在之前就被move了呢?故这里我们将loader的类型改写为&'window Fontloader
。这样,只要loader本身存活时间够长,就不会有冲突问题。