
昨天我们实现了一个有瑕疵的单栈链表,今天我们来优化下
我们刚搞了一个很小的链表,但是从头到尾一直有一种变扭的妥协,现在我们来优化下,绝不妥协!天王老子来了也不行!(难说)
接下来的流程:
peeking)在这个过程中,我们将对下面的知识有些许深入理解:
Option<T>的使用如果你对上面的四种知识存在至少一种不了解或者感到陌生,请出门左拐:rust基础学习--final: 章节索引 - 知乎 (zhihu.com)。
然后我们新建一个second.rs文件并将它添加到lib.rs中。
Option<T>回到我们之前设计的链表结构:
前面我们说过了,空和非空实际上可以变成有和没有。没有我们直接用None表示即可。
所以我们第一步先把可以用Option<T>替换的都给替换了,甭管好不好,不好再优化嘛。
其实也没多大变化,只是少了一些违和感。
接下来我们来用Option<T>提供给我们的一些方法处理掉另外一些变扭的地方:mem::replace,早看它不爽了!
我们这里有俩法宝可以替代mem::replace:
take:它相当于replace(None),也就是将Some里的数据拿出来,往里塞入一个None。insert:有拿自然就有放,将数据塞入Some里面(注意,当且只当insert的对象是None,如果原来就有数据则无效)不过目前我们只需要用到take,我们来替换掉mem::replace。
虽然差不多,但变扭感有所下降,用了Option<T>,自然要用它的api才对胃。
其中pop的那块代码还可以简化:
这段代码其实可以用map来实现,简单的说就是可以帮你Option<T>变成Option<U>,看个例子:
那我们这块代码也可以改成
ok,一阶段我们使用Option<T>替换掉所有变扭的方法和结构,接下来我们来继续优化下我们的代码
现在我们的链表只支持i32类型的,我们来让它通用一点。得益于Box的特性,我们的数据都是在堆上的,所以可以很舒服的实现泛型。
然后再来调整下impl部分
很基础,就不多说了。
我们目前只能通过pop拿到node保存的数据,但很多时候我们只是想看下数据,拿出来对比,并不想改动链表,这个时候pop就不行了,所以我们需要设计一个方法来获取数据。
不过需要注意,我们只是返回引用,不能直接拿出来。
你第一想法可能是下面这样:
但是你的第二想法应该就会立刻把它否定,或者你的编译器帮你否定。
我们前面看过map的代码,它会直接把原来的数据也就是head拿出来,而不是take出来,这对于&self来说是不可饶恕的。
我们只是想要一个引用,所以这个时候我们可以使用as_ref拿到head的引用。
这样就行了,因为这里的|node|变成引用类型&Box<Node<T>>,所以不用担心所有权问题了。
或者as_mut,这货一样会返回Option<&T>,不过是&mut T。
这俩在刷链表的算法题中非常常见。
然后我们来搞点测试用例:
但是这个测试用例遇到了问题,无法过编译:
我们明明在map中声明了|&mut value|了,为啥还是不能修改?
实际上这是个误区:闭包的参数是一个模式匹配,也就是说它会匹配符合&mut value的模式,那么value就是i32,整体组成一个&mut i32,我们自然是不能修改i32的,因为它immutable!。
ok,了解了这个误区之后,我们回到代码中,现在我们移除&mut就行了,这就代表着这是个可以修改的i32引用。
不过我们还是不能直接value = 42,为什么呢?因为这是个可变引用,直接修改的修改的只是指针,我们需要修改的是指针指向的那个空间的数据,这个时候我们就可以使用deref,也就是解引用,直接指向内存空间
根据我们之前学到的知识,我们直接使用*就好啦。
这样就没问题了。
从这里开始,复杂度上升!
迭代器是必要的,虽然链表在索引这块没有优势,但是我们还是不可避免的会用到,总不能每次都在pop吧。。。
要实现迭代的功能,我们需要给我们的List实现[Iterator](Iterator in std::iter - Rust (rust-lang.org))这个trait。
其中[Item](Iterator in std::iter - Rust (rust-lang.org))是我们迭代器中项的类型,我们一般称它们为关联类型(associated type),可自定义,写法为type Item = xxx(比如i32)。
这么做的好处是我们只需要写一处类型声明,其它地方全都是默认的Self::Item即可。
[next](Iterator in std::iter - Rust (rust-lang.org))是实现Iterator必须实现的方法,它定义了如何返回一个项。
不过这里有个问题,我们不仅仅需要实现T(IntoIter)一种类型,我们还需要&T(Iter)和&mut T(IterMut)。
我们先来实现IntoIter的,其实我们已经实现了类似next的方法,那就是pop,所以我们直接在里面使用pop即可。
简单的说下这里面的几个点:
tuple struct也就是元组结构体,作用是给List<T>实现IteratorList<T>实现一个into_iter的方法,用来返回一个迭代对象。你可能会疑惑:为什么要绕一圈,不直接给List<T>实现Iterator?
因为**List<T>不能自身就是迭代器!**
我们不能让它自己本身就是迭代器,这是一种越界。这算是一种规范。
但最主要的是我们还得实现Iter和IterMut(笑)
然后我们来搞几个测试用例:
ok,最简单的迭代器就搞定了,我们来实现剩下俩。
这货的难度比IntoIter高一个度,因为我们保留一个引用,也就是一个指针,这就意味着我们需要声明生命周期!
我们先来照猫画虎一下:
很明显有问题,我们来看下问题的具体:
作者:Oh god. Lifetimes. I've heard of these things. I hear they're a nightmare.
自然就是需要声明生命周期,因为编译器无法推导放在类型上的数据的引用会在什么时候挂掉,所以只能我们手动去声明。
也存在不需要声明的场景,比如:
这些都是编译器可以推导出来的,因为它遵守输入的生命周期最少都要比输出的活得久
换句话说就是输出按输入中生命周期最短那个算,这样必定安全。
你也可以在终端输入rustc --explain E0106
这里会告诉你怎么做是对的。
对于我们的这个代码,改起来也还算简单:
可以看到,生命周期的传染性和async/.await有的一拼,代码改完可读性减低一半。但这是必要的,因为我们没有**gc**!
类似的C或者C++对于处理空指针(悬浮引用)的问题上采用的是放任,有问题自行解决。
rust则是选择能管就管,毕竟对标的是它俩,对外宣传的重点就是安全,安全,还TM的安全!。
那么古尔丹,代价是什么呢?
自然就是开发的成本和复杂度。
扯远了,回到代码中。
上面的代码还是有问题:
简单地说就是我们写太多'a了,都在同一个函数里我们写它干啥,吃饱了撑着。
调整下,涉及到Iter里的T才需要声明,比如List<T>是没有必要声明的:
这回去掉没用的生命周期之后,
居然还有问题!!!
(╯°□°)╯︵ ┻━┻
问题也简单,类型问题,我们返回的是一个Box::<Node>,我们要的是Node,所以需要*一下,因为Box本身返回的是一个指针。
cargo,启动!
(ノಥ益ಥ)ノ ┻━┻
原来忘了as_ref
cargo, 启..启动!
😭
又多了个Box,因为*解的是as_ref这一层,Box的没解到。
我们直接改用as_deref,返回解引用的引用,也就是数据的引用,即&Node,相当于as_ref.map(|node| &**node)
另外我们还可以通过类型声明的方式让编译器帮我们解引用,比如:
因为map自身接收泛型,所以需要我们声明。这样编译器会自动帮我们*。
cargo,再带我们冲一次吧!!
这回终于可以了(流下心酸的眼泪)
然后我们来写测试用例:
这货难度自然和Iter差不多,但是难一点,因为是支持可变的,可变引用的限制比不可变引用的大很多。
我们先来写类型:
其中实现Iterator这部分相当于
然后根据上一小节复习的知识,next方法中可以省略掉生命周期。
然后我们来实现剩下的方法:
不过这里有个问题
因为next自身保管的就是&mut,如果你再分享一个&mut出去,那么就会同时存在两个&mut,这是不允许的。
另外这里补充一个知识:&是Copy的,连带着Option<&>也是Copy的,但是&mut不是,所以Option<&mut>也不是Copy的。
存放在堆上的数据一般都是Copy的,比如i32类型,它可以直接复制,为什么呢?因为它会自动调用Copy,复制一个也放到栈上(注意,如果你是&i32,那就是搞了一个指针也放在栈上指向这个数据)。
回到我们的问题中,既然不能同时存在俩&mut,那我们只能将next里面的take出来了
注意,这个时候self.next被take之后变成 None了,我们之所以这么做一方面是没办法很好解决所有权问题(要么用unsafe),另一方面是我们没办法拿到next的上一个,所以可以大胆一点,将next直接take出来。
现在我们可以直接通过引用去修改数据,这样就舒服很多了。
其实这段代码还是有瑕疵,Iter里面我们只能存一次和拿一次!
今天我们优化了昨天的代码,但是还存在一些问题,我们下面来处理这个问题。