这系列RUST教程一共三篇。这是第二篇,介绍RUST语言的关键概念,主要是所有权和声明周期等。
在写第一篇中的练习代码时,不知道你有没有尝试过连续两次执行vec_min
函数。这种做法在大部分其他语言中都属于正常行为,但如果你对rust这样做了,立即就得到一个error,编译都通不过:
“值在被移走后用掉了”!怎么会这样?
rust的宗旨是内存安全。为了实现这个任务,rust指定了一条铁律:通过别名访问数据时不能修改数据。这就是大名鼎鼎的“所有权”!
实际上这就是内存安全问题的根源:可变性以及起别名。可以看一下这些文章:Aliasing is what makes mutable types risky (Java), Aliasing and “Mutation at a Distance” (Python)
ownership
为什么要这样呢?我们通过一段C++程序来看这个问题:
void foo(std::vector<int> v) {
int *first = &v[0];
v.push_back(42);
*first = 1337;
}
传统程序里面,C++的内存需要我们程序员来管理。这里变量first
指向了参数v
的首地址,然后给这个数组插入一个新元素;如果插入的时候数组是满的,v
就会指向一个新的更大的数组,这样first
指向的内存实际是无效的了。这就是超名昭著的“悬垂指针”问题。
那rust怎么做的呢?
看一个小程序:
fn handle_vector(v: Vec<i32>) { }
fn ownership_test() {
let v = vec![1,2,3,4];
handle_vector(v);
println!("{}", v[0]);
}
肉眼看这个程序好像非常正常,但是给cargo build
瞅了一眼,它说最后一句有问题,编译不了! Son of a biscuit.
rust中,当你把一个变量传到其他函数中时,rust认为你主动出让了所有权,出让后你就再也不能访问这个变量了。在其他函数执行完成后,这个传进去的变量处的内存会被回收掉。如果允许你访问,就和上面说的“悬垂指针”一样的问题了。
那这和上面说的所有权规则有啥关系?所有权规则是:通过其他别名访问数据时不可修改。可是传给其他函数后(必然会起别名),数据可能被修改了,所以你就不能再访问了。
可是Java里面就是这样传进去的啊!也没出问题啊
rust也能实现Java这样的效果。——当然能,必须能;如果不能,我相信没人会使用rust了。
还是以我们的vec_min
函数为例,来看一下rust中的引用“借用”。
&
目前的函数签名是这样的:
fn vec_min<T: Minimum>(vec: Vec<T>) -> SomethingOrNothing<T> {}
假设你有一部iPad,你的朋友们都可以借用它来浏览网页,用完还给你,期间他的朋友可能也会借用一会;但是他们借走期间,你没法使用它了 —— 当然是这样,毕竟我们也遵循“泡利不相容原理”,噗。
rust也模仿的这种现实:传递参数时可以指明是在“borrowing”而非“moving”。之前我们写的代码都是对所有权进行了move,要改成borrow需要在参数类型前面增加一个&
。
fn vec_min<T: Minimum>(vec: &Vec<T>) -> SomethingOrNothing<T> {}
现在vec
不再是集合类型了,而是&Vec
类型。要想拿到引用对应的变量值,需要使用解引用符号*
:
fn vec_min<T: Minimum>(vec: &Vec<T>) -> SomethingOrNothing<T> {
let mut min = Nothing;
for e in vec {
min = Something(match min {
Something(i) => { e.compare(i) }
Nothing => { *e }
})
}
min
}
这里还涉及两处其他的改动:
- 你可以通过观察或者运行来发现,为什么
e
是一个引用类型,但是只有Nothing的分支进行了解引用,Something分支却直接调用了它的compare方法。所以这里需要去修改comapre方法,将第一个参数self
改成&self
pub trait Minimum : Copy {
fn compare(&self, s: Self) -> Self;
}
impl Minimum for i32 {
fn compare(&self, s: Self) -> Self {
if *self < s { *self } else { s }
}
}
相应的,实现的地方在返回的时候也要判断,返回self就使用*self
,返回s则不用加*
。
- 第二个问题比较难发现,需要给元素类型实现
Copy
特征,上面代码中已经增加了。
现在你可以试一下了,调用两次vec_min
并不会报错了。
&mut
借出去的所有权,数据可以被修改吗?
是可以的。
你可以尝试在vec_min
中给参数中push
或者remove
。应该会报错,根据错误信息响应的调整代码即可。你会发现,参数类型前面也可以加mut
,变成了这样:
fn vec_min<T: Minimum>(vec: &mut Vec<T>) -> SomethingOrNothing<T> {}
甚至这样写:
for e in v.iter_mut() {
*e += 1;
}
但是由于是引用类型,并不能赋值。你可以在调用前多次vec = vec![2];vec = vec![3];
,但是在vec_min
里面却不能执行这样的语句。
借用引用和可变引用
上面说的实际是两种不同的引用类型。rust中严格区分了他们。跟编程中的加锁一样,借用引用(用&
来开启)同一时刻是可以存在多个的,他们互相不影响,因为只有读操作 —— 就和读锁一样;而可变引用(用&mut
开启)同一时刻只能存在一个,而且使用了可变引用就不能使用借用引用了,因为什么?“不可重复读”。所以可变引用就是排它锁,可以称为 唯一引用 。
再来一个例子。
这次我们构造一个对象,类似于Java中的BigInteger
。这种对象能够表示非常大的数,内存有多大数就有多大(哈哈