Rust学习 I

Rust - I


Intros

​ Rust,好!可能主要由于所有权机制上的创新,学习时的感觉与学其他语言的感觉完全不同,于是没有像学JS一样,觉得无聊,也没有像觉像学haskell一样,觉得过于抽象。但是这样一种语言创新,必然会给学习带来障碍,毕竟编程思想是完全不同的。此外,可能我使用Rust的工具链不对,个人认为vscode对于Rust的支持明显不足(缺乏自动补全,没有函数快速查看以及定义跳转等等),第一天学的时候,只能实现一些强逻辑性算法(比如什么快排,归并排序等等),无法深入使用数据结构(给我一个数据结构我根本不知道里面有什么方法)。

​ 第一天快结束时,想学习一下Rust的可视化工具Plotters,结果发现,之前从菜鸟教程了解的写法过于粗浅,基本看不懂Plotters代码,遂投身更加深入的学习。但却发现,给自己设置的小目标 --- 写一个链表,按照之前了解的语法知识,我都是写不出来的。快要放弃只是接触到了一个教程以及其官方文档:

​ 教程详细介绍了对于链表的实现,较为通俗易懂,有些难以思考的问题,其实沉下心来想也很快能想出来。本文是跟着教程实现过程中,笔者对于遇到的一些问题的处理方法以及自己的心得。由于笔者非常不喜欢依葫芦画瓢(因为这样,感觉自己完全学不到东西),所以笔者也在自己的实现中整活(超前学习),本文也记录了整活过程中遇到的坑及处理方法。本篇为Rust学习心得的第一章。


II. 用Rust实现链表

2.1 null pointer optimization的意义?

​ 假设我们有一个这样的数据结构:

1
2
3
4
enum Foo {
Empty,
NotEmpty(a_pointer)
}

​ 通常来说,enum类型Foo(可以将enum的内存功能理解为一个union)需要有足够空间以存储其表示的数据,也即在Empty以及NotEmpty两个类型之间,选择所占空间最大的那个类型对应的内存消耗作为enum类型Foo的大小。但是,Foo如何才能知道当前表示的是Empty还是NotEmpty类型?通常,需要额外的空间(比如一个字节)存储一个叫tag的域,以指示当前Foo的真正含义。有一种例外,例如以上这个例子:

(1)Empty 没有关联数据

(2)NotEmpty 我们认为是一个指针,对于此指针而言,数据位为0(空指针)是不允许的

​ 也即,此例子中,NotEmpty中不被允许的结果(全0)恰好可以被用于表示Empty。则此时,我们不再需要tag域来表示Foo的实际意义(Foo中数据可以直接判定)。

2.2 错误 - private type 'xxx' in public interface

​ 如果我直接写如下代码:

1
2
3
4
struct Foo {elem: i32}
pub struct Bar {
test_elem: Foo
}

​ 这样是完全没有问题的。但是如以下这两种写法,都会出现问题

1
2
3
4
5
6
7
8
struct Foo {elem: i32}
pub struct Bar { // 第一种写法
pub test_elem: Foo
}
pub enum Bar { // 第二种写法
First(Foo),
Second
}

​ 两种写法都会报错,编译器认为:can't leak private type。struct中的权限,不同于C++的结构体(默认public),struct默认private,如果显式定义了pub关键字才成为public的。而对于enum而言,只要enum本身是public的,其中的所有元素都是public的。但Rust不允许用户在公开的类型中,暴露私有类型,正如参与国家机密项目的科学家,即使这个科学家(的存在)并非机密,他也不能随意讨论机密的内容。

2.3 mem::replace的作用

​ 如果我们使用链表实现stack,也即不记录链表的尾部,假设我们需要push新的值到链表中,就会涉及到修改链表的表头:我们需要新创建一个Node,此Node的head是原来链表的head(stack堆积性),假设Node的两个域:data(数据)与next(链表指向下一个元素的“指针”),有两个内容需要修改:

(1)head本身的值,需要修改为最新的Node,这个只需要修改head的指向

(2)新Node的next,由于我们需要原来的head作为next,这里涉及到移动或者复制。如果用复制:我很惊讶我瞎搞一通竟然过编译了(见下代码块),首先,定义一下stack使用的列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pub struct List {
head: Link,
}
enum Link {
Empty,
More(Box<Node>),
}
struct Node {
elem: i32,
next: Link,
}
impl List {
pub fn new() -> Self {
List { head: Link::Empty }
}
}

​ 那么假设我们需要写一个push,就涉及到了将head赋值给next的操作:

1
2
3
4
5
6
7
8
impl List {
pub fn push(&mut self, elem: i32) {
let new_node = Node {
elem: elem,
next: self.head,
};
}
}

​ 但是直接这样是不允许的,因为self.head 会被move掉,move会使得当前链表的head直接被销毁,也即链表部分被摧毁了(或者说成,partially-initialized,有一部分没有初始化)。如上,在同样的位置(next处)使用clone,如注释所示,也是不允许的。编译器提示:clone是没有被实现的特性(self.head没有Clone函数),那么我们来实现一下。考虑到head是一个Link,那么我们把Link的clone函数实现了,是不是就可以了?如下所示:

1
2
3
4
5
6
7
8
impl Clone for Link {
fn clone(&self) -> Link {
match self {
Link::Empty => Link::Empty,
Link::More(next) => Link::More(next.clone()),
}
}
}

​ 其中需要注意的点:Link的enum语法特性,需要判定当前Link的实际意义(match)。如果是Empty 需要返回Empty,请使用namespace指定 返回的EmptyLink中定义的。 如果是More,这里需要注意两点:

(1)Link::More(next) 此处的next是一个shared reference,如果要用来初始化则需要解引用*next。但是很遗憾,这里会报错,直接*next是一个移动操作。会使得Link成为 partially-initialized的数据,这是不被允许的。

(2)直接使用next.clone()。很抱歉,这里又有问题:

  • the method clone exists for struct Box<Node>, but its trait bounds were not satisfied
  • struct Node ----------- doesn't satisfy Node: Clone

​ 个人猜测,这是由于Node本身没有实现clone函数导致的,于是这里我实现clone。

1
2
3
4
5
6
7
8
impl Clone for Node {
fn clone(&self) -> Node {
Node {
elem: self.elem,
next: self.next.clone(),
}
}
}

​ 这样竟然过编译了,并且,结果也是正确的,说明这样的方法是有效的,可以通过暂时保存一份原数据的copy。但是这样是否更慢?测试显示,这种需要clone的方法,在同样进行2000次push的情况下,比官方文档中使用std::mem::replace的版本,慢将近2000倍... 所以还是好好学习官方实现吧。

Time elapsed for my push: 309.202965ms Time elapsed for swap push: 160.718µs

2.4 在循环中使用match方法

​ 学习教程的时候我们已经知道了,对于只有两个分类的enum,可以使用match的语法糖:

1
2
3
4
5
6
let ptr: &Link = &stack.head;
if let Link::More(node) = ptr {
...
} else {
...
}

​ 注意,Link::More(node)本身是enum类型(此例子中,是Link)的一个常引用(shared ref,之所以称之为常引用,是因为个人认为这个与C++中的常引用非常类似),而node则是对应的Link::More类型的常引用,此处也就是&Box<Node>类型的。那么根据这个语法糖,可以写出一个遍历栈的方法:

1
2
3
4
5
6
7
pub fn show_stack(&self) {
let mut ptr: &Link = &self.head;
while let Link::More(node) = ptr {
println!("Stack: {}", node.elem);
ptr = &node.next;
}
}

​ 首先,ptr需要是【可变的】常引用(相当于const type *),才能改变其指向。我们判定,如果ptr不是Link::Empty,由于此时ptr实际对应了一个&Box<Node>,我们将ptr对应的Node中元素取出,并使得ptr指向下一个元素(注意,是node.nextLink类型的,但我们需要的ptr是对Link的引用,故需要符号&

2.5 关于泛型编程(generics)

​ 我想直接使用模板类型T作为栈的类型。于是需要修改定义:

1
2
3
4
5
6
7
8
9
pub struct List<T> {head: Link<T>}
struct Node<T> {
elem: T,
next: Link<T>,
}
struct Node<T> {
elem: T,
next: Link<T>,
}

​ 所有相关位置都需要增加<T>以指示当前类型是模板类型。需要注意以下几个问题:

(1)如果要初始化模板类型,在类型声明时需要带有<T>,而构造时(等号右边)不需要,比如:let new_node: Node <T> = Node {...};,如果右边带了模板,则会报错:chained comparison operator。类型声明同样也包括返回值。如果不声明类型,则会进行自动类型推断,绑定类型。T根据先后顺序进行类型绑定,例如Node的两个变量都是类型T,而初始化时,由第一个变量传入的f32绑定T = f32,而第二个变量则是i32,产生冲突,则会报错。

(2)impl需要有<T>,也即例如impl<T> ... Link<T>

​ 但是事情并没有那么简单,不是改好了所有的模板声明与定义就能通过编译了(sad)。这里存在的一个问题时,我之前实现了一版基于clone方法的push,那么想要正确应用clone,需要 类型支持clone方法。而原来使用的i32,由于是基本类型,在Node进行clone时直接按值传递,无需clone,现在由于是未知的类型T,由于其很可能不是基本类型,故直接 elem: self.elem 默认引发 move。

​ 想要避免move,简单的想法是我直接 elem: self.elem.clone()。聪明吧?不太聪明。self.elem作为类型T的变量,需要实现Clone方法。对于类型T而言,我不知道怎么实现它的Clone方法(只学了两天,巨菜的),那我只能限制类型T包含了Clone特性了。于是:

1
2
3
4
5
6
7
8
9
pub struct List<T> where T: Clone {head: Link<T>}
enum Link<T> where T: Clone {
Empty,
More(Box<Node<T>>),
}
struct Node<T> where T: Clone {
elem: T,
next: Link<T>,
}

​ 其中,where限制了T需要存在的特性,这被称为trait bound。没有满足就会报错:E0277: required by this bound in xxx 。对应地,我们需要在impl块中,告知编译器类型T已经实现了某个trait,也即impl<T: Clone>

​ 这样还没完!之前写的函数还是存在问题,猜猜改了什么?

1
2
3
4
5
6
7
8
9
impl<T: std::fmt::Display + Clone> List<T> {
pub fn show_stack(&self) {
let mut ptr: &Link<T> = &self.head;
while let Link::More(node) = ptr {
println!("Stack: {}", node.elem);
ptr = &node.next;
}
}
}

​ 答案是,将impl<T: Clone>修改为impl<T: std::fmt::Display + Clone>。这是因为Rust编译器认为,类型T是什么啊?我怎么display它啊?你实现了Display方法没?显然我没有。那么我们是否有必要在整个stack中都添加此约束呢?比如pub struct List<T> where T Clone + std::fmt::Display?答案是,可有可无。如果不加,对于一些复杂的自定义T而言,不能直接调用show_stack方法了。也即,impl块在此处,只限制了对于此方法的调用。没有实现std::fmt::Display特性的类型,不调用这个函数就行了,其他功能并不影响。P.S. 注意多重约束的写法是加号。P.S.2,为了简洁起见,我可能会考虑删除基于clone的版本,使得复杂类型T不一定需要实现Clone特性。P.S.3,注意,在type用法中,如果type alias是一个模板类型,则无需写特性约束(写了也会被编译器忽略并且报一个warning的)。

2.6 关于所有权,租借以及move

​ 是时候写pop了。pop,很显然需要考虑栈是否空。由于栈是否空由enum的状态表示,这里直接使用match即可,我使用了一下match的语法糖:

1
2
3
4
5
6
7
8
9
pub fn pop(&mut self) -> T {
if let Link::More(node) = &self.head {
let pop_elem = node.elem.clone();
self.head = node.next;
return pop_elem;
} else {
panic!("Can not pop from an empty stack.");
}
}

​ 这里有两个需要注意的点:

(1)进行match操作时,如果直接写let Link::More(node) = self.head,会发生move(毕竟有let在这里嘛,可以理解成node是一个新创建的变量,self.head将变更所有权)。我们并不希望self.head丧失所有权(而被销毁),则可以通过一个可变引用来使用self.head。涉及到引用的地方是不可以发生move的,因为引用只是租借了变量,获得了临时的访问或修改能力,就像住房问题,共享引用(shared)只可以使用,而不能修改(比如如果选择住酒店,就不能随意装修),与之相比,可变引用就像是对房子的长租,可以进行装修。但这两种引用都不改变原房主对房屋的所有权,此时如果发生move(所有权的转让),则是在进行违法犯罪活动(没有所有权的人,要把当前房屋的所有权转移给别人)。故self.head之前需要增加所有权引用。

(2)由于增加了引用,此处的node也是原self.head实际数据的引用(&Box<Node<T>>)。那么这就涉及到一个问题,self.head是值(value),我们希望用self.head.next (实际不能这样访问,我这里只做一个示意)来重写self.head。但node是一个引用,我们在self.head = node.next的过程中,会将node的next项move给self.head,对引用进行的move,是不被允许的。果不其然,最后报错了。

​ 此处依然可以使用mem::replace。注意mem::replace的性质:

1
pub fn replace<T>(dest: &mut T, src: T) -> T

Moves src into the referenced dest, returning the previous dest value.

​ replace将目的位置用src替换,并且 按值 返回dest位置的变量。正好,我们需要self.head按值而非按引用返回的结果。这样获得的node就可以进行move(因为按值传递的node具有所有权)。则可以简单重写第二行:

1
if let Link::More(node) = mem::replace(&mut self.head, Link::Empty) {...

​ 这样就可以过编译了。P.S:如果需要可控的错误控制,可以返回Option<T>,当Empty时返回None,就不用不可恢复的panic宏了。

2.7 Drop特性的实现

​ 由于原本的stack链表,自动析构过程不能进行尾递归优化,为了防止析构时爆栈,需要手动析构,也即实现Drop特性。直接实现如下:

1
2
3
4
5
6
7
8
impl<T: Clone + Default> Drop for List<T> {
fn drop(&mut self) {
let mut ptr = &mut self.head;
while let Link::More(node) = mem::replace(ptr, Link::Empty){
ptr = &mut node.next;
}
}
}

​ 看起来好像很有道理?首先,我将ptr指向的本节点换出(到值),同时使得原变量置为Empty,再使得ptr指向换出后的值对应的next。但这样写是有问题的,因为我这样实现,利用了一个特性:我将ptr换出到值,此值是临时变量,在结束本次循环之后,会析构,也即此node无效了,并且原变量也设成了Empty。但我对node.next进行的引用操作,阻止了我利用临时变量短生命周期的特性,ptr指向的内容将可能无效。故这样写会触发编译错误。

​ 正确的写法仍然是,按值传递:ptr应该一直是下一个node的值,可以写成:

1
2
3
4
5
6
7
8
impl<T: Clone + Default> Drop for List<T> {
fn drop(&mut self) {
let mut ptr = mem::replace(&mut self.head, Link::Empty);
while let Link::More(mut node) = ptr{
ptr = mem::replace(&mut node.next, Link::Empty);
}
}
}

​ 这样,每一个node原有的变量都会由replace方法,将所有权转移到ptr中,原变量全部置为Empty。注意,由于是按值转移的,给原变量赋值Empty也并不会妨碍我们在replace后使用ptr取出其中的node,因为所有权以及变量真正的信息已经转移到了ptr中。其中需要注意的两点:

(1)node这玩意,如果不加前面的mut,是错的。因为这里我们写的是let <variable> = ... 这样的句式,我们定义了一个变量node,但是没有将其定义成可变的。对于不可变变量,不能使用可变引用,在下一行的replace处会报错。对while let句式中,等号前面变量,也需要理解成是一个正规的变量定义过程。那么此处,将while let写为标准的match形式,也有同样的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
impl<T: Clone + Default> Drop for List<T> {
fn drop(&mut self) {
let mut ptr = mem::replace(&mut self.head, Link::Empty);
loop {
match ptr {
Link::More(mut node) => {
ptr = mem::replace(&mut node.next, Link::Empty);
},
Link::Empty => {
break;
}
}
}
}
}

​ 这里需要注意的是,loop与match的配合用法(在多类型enum下使用的逻辑)。则根据这个例子,我们可以认为,match的过程中可能定义新的变量,那么此变量的性质可以根据括号内的内容进行确定。

(2)还有另一种写法while let Link::More(node) = &mut ptr,我们来分析一下为什么也可以过编译:这样写,node是ptr的一个可变引用。如果需要使用结构体中的某个值,自然可以可变引用自这个结构体的可变引用了。此处的引用不会叠加,&mut node.next不会称为引用的引用(因为node.next不是引用,node才是)。故replace后,可以把node,next处对应的变量所有权移动到ptr中,变量的类型(ptr是Link<T>)也可以对应replace的结果。


III. Stack with Option

3.1 转向Option

​ 不难发现,前面的enum Link就是一个弱化版的Option。至于为什么是弱化版的,答案很显然,Option作为一个标准库提供的模块,自然有更多方便使用的函数。其中一个就是take函数。我们可以看take函数的定义:

1
pub fn take<T>(dest: &mut T) -> T 

Replaces dest with the default value of T, returning the previous dest value.

​ 这里有两种形式的take,一个是std::mem::take(官方文档非常清晰),另一种是:实例化的Option可以调用的take函数。两者的功能是类似的,只不过mem::take针对所有类型。查看Option的官方文档可以知道,take返回Option自身的同时,将原变量设为None,相当于进行所有权转移的函数,用此函数可以避免move。而对于mem::take,官方文档给了一个简单易懂的例子:

1
2
3
4
5
6
7
8
9
struct Buffer<T> { buf: Vec<T> }
impl<T> Buffer<T> {
fn get_and_reset(&mut self) -> Vec<T> {
// error: cannot move out of dereference of `&mut`-pointer
let buf = self.buf;
self.buf = Vec::new();
buf
}
}

​ 上面的error说的就是:由于函数传入的是self的可变引用,不能被move,故报错。

​ 那么,使用take函数,可以简化原来使用mem::replace的位置,如drop函数:

1
2
3
4
5
6
7
8
impl<T: Clone + Default> Drop for List<T> {
fn drop(&mut self) {
let mut ptr = self.head.take();
while let Some(mut node) = ptr{
ptr = node.next.take();
}
}
}

​ 只需要记住,ptr需要接受按值传递的结果,这样不会引起move问题。此外,Option还实现了两个重要的方法:map以及unwrap

​ map,有那么一些类似于python中的map。python中的map可以将一个iterator中的所有元素通过某个函数进行映射。而此处的map,是将Option进行变换。官方的定义是:map可以通过传入的函数,将Option<T>转换为Option<U>

1
pub fn map<U, F>(self, f: F) -> Option<U>

Maps an Option<T> to Option<U> by applying a function to a contained value.

​ 官方文档提供了一个这样的例子。对于Option<String>类型的变量来说(也就是带有None选项的String),直接求len()是非常繁琐的,我们需要通过好几行match指令块或者if let语法糖才能得到其长度。而对于map而言,由于传入的函数,操作的是T,本质是一个T->U的映射函数,可以很轻松地获得其长度:

1
let result = some_string.map(|s| {s.len()});

​ 那么,map的变体有map_or以及map_or_else,我觉得比较有价值的是map_or。其实不光是map函数有_or以及_or_else实现,其他一些函数也有这样的实现,如之后要说的unwrap, okor本身也是一个函数。map_or的输入则是:

1
pub fn map_or<U, F>(self, default: U, f: F) -> U

​ 也即需要提供一个默认值。也即当结果为None时,返回一个默认的结果。map_or_else则是,传入两个函数,如果当前值非None,则执行第二个函数(f),否则执行第一个函数。

​ 而unwrap,根据其名字理解,也即解包。显然,在Option中,我们将有意义的值包在了Some中,但通常我们可能需要内部的值。使用match块同样太过复杂,我们考虑用unwrap,这个函数实际就是一个Option<T>到T的映射。同样地,其_or以及_or_else方法,都有类似的map对应函数的函数思想。值得一提的是,unwrap_or_else中有这么一句话:

Returns the contained Some value or computes it from a closure.

​ 闭包?这里说的实际上是我们传递的匿名函数,写法就是|Some的内部变量| {有返回值的函数体}。此函数与C++的匿名函数很像,也可以从外部捕获变量(捕获的变量也一样无需写在|·|(或C++中[·])中),用在函数体中进行计算。

​ 知道map的工作原理之后,我们可以立刻用map进行一些程序改写。比如stack中的pop,我们希望在修改head的同时,可以返回原head对应node的值。则可以按照如下方式进行书写:

1
2
3
4
5
6
7
8
9
impl<T: Default> List<T> {
pub fn pop(&mut self) -> T {
self.head.take().map(|node {
self.head = node.next;
node.elem
}).unwrap()
)
}
}

​ 此处的逻辑很简单:首先take()将self.head置None,并且返回实际存储的Some(node),Some(node)进入map中的函数被处理,取出其中的node(按值),重新覆盖self.head(node变量内部值的move),按值返回node的elem域。但注意如果没有.unwrap(),如上返回的Some(T)或者None,如果要返回真正的T类型值,则需要加上unwrap,但这不是很危险,当stack为empty时,也不知道会发生什么(我们删去了empty处理)。

3.2 as_ref | as_mut | as_deref

​ 之前我想实现一个top函数,不过此top函数是返回值的函数,而官方教程则说:我们应该实现一个返回引用的top(教程叫做peek)。开始我觉得这好像也不是什么难事吧?于是我写了一波:

1
2
3
4
5
pub fn top(& self) -> &T {
self.head.map(|node|{
&node.elem
}).unwrap()
}

​ 结果直接报错了,错误信息:

&node.elem returns a reference to data owned by the current function

self.head.map(|node|{... move occurs because self.head has type Option<Box<second::Node<T>>>, which does not implement the Copy trait

​ 我的理解是,self.head.map由于实际是match方法的一种简化形式,那么根据match倾向于move值这个特性,self.head被隐式move了。这样会导致错误。而与原先不同,我们不能使用take()函数(take会使得原来的self.head被设为None,同样一次take的结果也不能被move到多个变量中)。教程上推荐使用as_ref方法,于是我在官方文档上查了一下这个函数。看到这个函数中的其中一句话,我终于透彻地理解了文档中的两句话的意义:

  • "Consume the original" 表示原变量将被销毁(被move,所有权丧失)
  • "Preserving the original" 表示保留原变量

​ 但是as_ref文档中所说的:"Converts from &Option<T> to Option<&T>." 具体表示什么意思呢?引用符号在内部外部的区别是什么?我是这么理解的,仍然以stack中的内容为例子。stack的Link,其类型是Option<Box<Node<T>>>。那么上面这句话的意思就是将&... 转化为Option<& Box<Node<T>>>。为什么要这样呢?我们先回顾一下map的性质。我们在函数的参数列表中写的是& self,这就导致了self.head.map无法进行,这是因为,由于map具有类似match的属性,会导致self.head发生move(到新产生的变量node中,如果还不是很明白,请参考本博客的这里),引用内部是不允许发生move的,故报错。而take则将内部的Node取了出来,但是是按值取的,原来的head内容变成了None,并且难以恢复。故这里,我们希望以一种不破坏原来的self.head的方式,返回一个对self.head中node的引用。注意到:由于参数列表中&self的存在,这里的self.head实际上 就是&Option<Box<Node<T>>>

​ 如果将其转化成Option <&Box<Node<T>>>,那么map得到的结果node,就是一个引用(&Node<T>),我们可以通过如下代码来确定:

1
2
3
4
5
fn top_node(&self) -> &Node<T> {
self.head.as_ref().map(|node|{
node
}).unwrap()
}

​ 注意,由于以上代码只是测试使用,我没有定义成public函数(事实上,定义成public函数会报错,Node是一个私有类型,被放在了公共区域,private type leak错误)。这段代码是可以过编译的(放在对应的impl块中)。则说明,node本身就是一个引用。故不发生move,self.head被成功保存下来了。太好了,我们又赢啦!那么,最后如果要返回&node.elem,只需要在self.head之后,加上.as_ref()即可。

​ 那么余下的两个函数,根据分析as_ref时的经验,理解起来应该会容易很多:

  • as_mut: Converts from &mut Option<T> to Option<&mut T>. 也即,当结构体成员函数使用&mut self输入时,as_mut后接的map将可以输出node的可变引用
  • as_deref: 我错了,我暂时不想去递归deref以及Deref::Target是个什么玩意。

​ 这里我想再按照自己的理解,解读一下官方教程的例子。官方教程想通过自定义的peek_mut修改栈顶数据,但是:

1
2
3
list.peek_mut().map(|&mut value| {
value = 42
});

​ 这样写是显然不可以的。首先,peek_mut就如我们的top_node_mut一样,返回的是&mut T (官方教程则是返回Option<&mut T>),在map之后,将成为&mut T,也即,value本身就是一个mutable reference,那么此例子中,对于一个mutable reference,再进行引用,成了mutable reference的mutable reference,会怎么样呢?首先,mutable reference变量value,只是说 其指向的内容是可变的,但并没有声明,value本身可以变(也即value不可以改变指向,但是能改变指向的内容),只有value本身有mut属性,才能安全地对value进行mutable reference。这里显然没有这样的条件,并且即使挂上了这个引用,最后修改的也是value值,而不是value指向的值。


Rust 其他TODO项

  • 值得一记,回头搞清楚原理(主要是dyn关键词用法):使用函数作为参数进行传递(callback类写法)
1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
sort_test(& bubble_sort);
}

fn sort_test(sort_func: &dyn Fn(&mut [i32])) {
let mut arr = [7, 5, 2, 8, 9, 0, 3, 3, 5, 2, 7, 10, 3, 2, 5, 6];
sort_func(&mut arr);
}

fn bubble_sort(arr: &mut [i32]) {
...
}
  • Vec 切片的to_vec函数与切片类型(切片转数组或其他数据类型时的overhead)
    • Vec切片之后会变成 &[type]?比如&vec[3..9] 实际上是& [i32]?
  • cargo new --lib <name>lib具体起什么作用?
  • Box 说是堆的指针?其作用类似于C中动态分配内存的结构?