别的语言,学习的差不多了。写个链表试试?
Rust,学习的差不多的,写个链表,逝世!
虽然rust是系统及编程语言,但那和safe代码没有什么关系,在safe代码中,你依旧不能灵活的操纵内存。
代替的可以使用Box来当作指针,但是由于所有权和借用的规则,使得Option+Box这种形式写出的LinkedList既复杂,又低效。但是对于学习来说还是很有帮助的。所以,本篇文章将编码一个支持基础操作的LinkedList,并解释说明那些烦人的细节。
希望本篇文章能对你对Rust的理解有帮助。本文章基于《通过链表学rust》。
我们要写一个泛型的链表。使用Option
这样的形式,来组织一个最基本的链式结构。
使用类型别名是由必要的。Link是如下的别名
type Link<T> = Option<Box<Node<T>>>;
这样可以避免冗长且不易阅读的代码。然后请你记住一个重要的,Link不是一个指针类型,你很可能基于C风格语言的习惯将Link看作一个指针——在我们这个例子中不是这样的。但可以做到这样,那是unsafe的事情了。在我们例子中,**Link是一个Option。**使用Option
可以模拟出类似指针的东西。但是你在处理LinkedList的时候大可将其当作一个指针来看待。
我们需要一个T类型的数据,和一个next来“指向”下一个Node。
struct Node {
data: T,
next: Link,
}
只需要一个头指针,就可以找到整个LinkedList。这个head类似于C++中的Node*
,在构造的时候head = nullptr
,就是说,头指针不默认指向一个节点。
pub struct LinkedList {
head: Link,
}
为什么没有提供size?
对于单向链表的size相关操作,需要额外的维护成本。如果使用单项链表,不要求size操作。否则使用双向链表。
new关联方法,返回一个初始化的LinkedList,只需要将head初始化为None即可。
pub fn new() -> Self {
LinkedList {
head: None,
}
}
头插入一个节点。
在编码之前,我们需要想想如何进行节点的额移动。
首先,这个操作会改变链表,所以需要&mut self做参数。
在方法体中,新建一个节点,并赋予初值。
让新建节点的next指向头
头指向新建节点
这些操作,你在数据结构中已经看见过了,而且身经百战的你,在经历各种语言的摧残之后,心想:这还不简单?但是rust却给了你当头一棒。你试尽了你能想到的所有办法,却只写出一段自己都不愿意阅读的,撇脚的代码。这就是作者的真实经历。
“独学而无友,则孤陋而寡闻”。是时候看一看别人是怎样写的了。
pub fn push_front(&mut self, val: T) {
let new_node = Box::new(Node {
data: val,
next: self.head.take(),
});
self.head = Some(new_node)
}
我们来一点点分析
创建一个新的节点
let new_node = Box::new(Node {
data: val,
next: self.head.take(),
});
其中take方法会将Option设置为None,并返回原先的值。
这几行代码新建了一个节点,还有将head滞空(赋值为None)。
将head指向新建节点,这步只需要将被some新建节点赋值给head即可。
self.head = Some(new_node)
因为self是&mut, 所以这里直接使用=就可以完成。
peek方法返回Option<&T>
。
你可能会想到match匹配。可以这样做。
pub fn peek(&self) -> Option<&T> {
match &self.head {
None => None,
Some(node) => Some(&node.data)
}
}
&具有最低的优先级,即&(self.head)
&self.head
是必要的。match不会消费对象,但是结构SomeSome(node)
会转移所有权。就会得到一个编译错误。那是因为我们移动一个已经被借用的值Box obj
。这个值已经被self借用了,你却要把它移动给node。
与peek相同,但是返回可变引用
pub fn peek_mut(&mut self) -> Option<&mut T> {
match &mut self.head {
None => None,
Some(node) => Some(& mut node.data)
}
}
同理的,match
应当匹配&mut self.head
。
pop究竟该如何操作,我想你在学数据结构的时候都已经快听的耳朵起茧子了额。但是在这里,还是要复述一遍。
用Rust语言来描述,可以用match来匹配head是否为None,如果是返回None,如果不是,会匹配出原来的head叫做node,然后更新head,返回node中的数据即可。
我们可以改变以下实现手法,使用take方法,取得head的所有权。然后再重新赋值head。
pub fn pop(&mut self) -> Option<T> {
match self.head.take() {
None => None,
Some(node) => { //node 为原来的head
self.head = node.next; //node.next: Link 变为新头
Some(node.data)
}
}
}
我们可以不实现drop,用编译器为我们实现的——调用head的drop。于是drop Box中的对象,然后Box中的对象是一个Node,Node中又含有Link,这会是一个尾递归?不是的,编译器生成的drop会含有其他的操作。这不是一个尾递归。
我们需要手动的为LinkedList实现drop这个trait。具体的做法是
把递归改为迭代,每次获得一个节点的所有权,让后让其离开作用域自动drop,因为是一个循环,所以不会出现栈溢出这种情况。
impl <T> Drop for LinkedList<T> {
fn drop(&mut self) {
let mut curr = self.head.take(); //获得head的所有权
while let Some (node) = curr { //让node绑定的对象离开作用域自动drop
curr = node.next;
}
}
}
为LinkedList提供迭代器。他们分别是Iter
,IntoIter
,IterMut
。其中Iter
,IterMut
需要额外注意生命周期。
IntoIter是最好实现的。因为其再迭代的时候会消耗容器,所以只需要每次pop以下就好了。
struct IntoIter<T> {
list: LinkedList<T>,
}
impl <T> Iterator for IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
self.list.pop()
}
}
Iter就相对比较难一点,我们需要获取容器的不可变引用。并迭代。如何指向链表的下一个节点,是一个问题。
Iter中定义一个curr: Option
,指向当前的节点,每次调用next,就返回curr的值,然后更新curr。
还是可以使用match来匹配。
pub struct Iter <'a, T> {
curr: Option<&'a Node<T>>,
}
impl <'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
match self.curr {
None => None,
Some(node) => {
self.curr = node.next.as_deref();
Some(&node.data)
}
}
}
}
由于self中的curr为Option包裹的引用类型,所以match匹配出来的值也是一个&类型,不用再额外添加引用。、
Some的部分值得拿出来好好说说。
Some(node) => {
self.curr = node.next.as_deref();
Some(&node.data)
}
先搞清楚类型,curr: Option
,node.next: Option
。我们直接使用 self.curr = node.next
来赋值,类型不对。Some(&Node) <- Some(Box
,要是给Box解引用就好了。使用as_deref
方法来返回一个解引用(被Some包裹的)。正好合适。
然后返回node中的data即可,别忘了添加&。
IterMut又设计到了可变性。编码要考虑的因素就更多了。
老样子,我们对Iter使用的方法,故技重施看看。
pub struct IterMut<'a, T> {
curr: Option<&'a mut Node<T>>
}
impl <'a, T> Iterator for IterMut<'a, T> {
type Item = &'a mut T;
fn next(&mut self) -> Option<Self::Item> {
match self.curr {
None => None,
Some(node) => {
self.curr = node.next.as_deref_mut();
Some(&mut node.data)
}
}
}
}
会得到这样的编译错误。
error[E0507]: cannot move out of `self.curr.0` which is behind a mutable reference
--> src\first.rs:88:15
|
88 | match self.curr {
| ^^^^^^^^^ help: consider borrowing here: `&self.curr`
89 | None => None,
90 | Some(node) => {
| ----
| |
| data moved here
| move occurs because `node` has type `&mut Node`, which does not implement the `Copy` trait
编译器说我们移动了以已经绑定可变引用的变量。
奇怪,为什么Iter可以而IterMut不可以呢?原因是不能对&mut & T
进行copy,就是没有实现Copy trait
,那怎么办?。那就又回到了peek的情况——移动了一个已经被借用的变量。只不过这个变量是一个可变引用&mut
。
按照编译器的方法改。你大可试试,会有更多的错误。换一种方法吧。
还是take方法。
pub struct IterMut<'a, T> {
curr: Option<&'a mut Node<T>>
}
impl <'a, T> Iterator for IterMut<'a, T> {
type Item = &'a mut T;
fn next(&mut self) -> Option<Self::Item> {
match self.curr.take() {
None => None,
Some(node) => {
self.curr = node.next.as_deref_mut();
Some(&mut node.data)
}
}
}
}
LinkedList的这三个方法如下定义。
pub fn iter(&self) -> Iter<T> {
Iter {
curr: self.head.as_deref(),
}
}
pub fn iter_mut(&mut self) -> IterMut<T> {
IterMut {
curr: self.head.as_deref_mut()
}
}
pub fn into_iter(self) -> IntoIter<T> {
IntoIter {
list: self
}
}
fn main() {
let mut ls = LinkedList::new();
ls.push_front(12);
ls.push_front(14);
ls.push_front(100);
ls.push_front(222);
ls.pop();
assert_eq!(ls.peek(), Some(&100));
assert_eq!(ls.peek_mut(), Some(&mut 100));
for val in ls.iter() {
println!("{}",val);
}
for val in ls.iter_mut() {
*val += 1;
println!("{}",val);
}
for val in ls.into_iter() {
println!("{}",val);
}
}
很好的工作了起来!
再来看看数据量大的时候,会不会栈溢出。
fn main() {
let mut ls = LinkedList::new();
let n: usize = 10000000;
for i in 0..n {
ls.push_front(i);
}
}
很好,栈没有溢出。
如果谁犯下了大罪。我一定让他试试在不看资料的情况下手写一个Option+Box的链表。
玩笑归玩笑,好好的看清楚上面的实现,还是能对所有权和借用和声明周期的理解更上一层楼的。
对于peek,peek_mut等函数,你也可以使用map来实现,但是使用map会获得所所有权,又需要额外的处理。本文保留了match方法。