从C++的RAII到Rust的所有权(一)
Contact me
- Blog -> https://cugtyt.github.io/blog/index
- Email -> cugtyt@qq.com
- GitHub -> Cugtyt@GitHub
如果你有C++的经验就知道,在C/C++里面变量之间默认都是复制的,从C语言的函数传参谈指针说的就是C中的变量复制,C++中如果你没有让构造函数做移动或其他特殊操作,那么变量也是复制的,比如:
Class C { /* sth. normal*/ };
C c1;
C c2 = c1;
c2是把c1复制了一份,如果是复制比较廉价还好,如果对象比较大的话,复制就比较昂贵,那么我们一般的做法是用指针,指针的复制很廉价,当然考虑到实际环境,你可能要考虑使用现代C++的智能指针,这就有好几种选择了,从C++的RAII理解智能指针的思路(一)和从C++的RAII理解智能指针的思路(二)从RAII的角度做了智能指针的基本解释。当然如果要和老代码打交道,还需要考虑普通指针和智能指针的共存。而且使用指针必须注意指针臭名昭著的问题,例如野指针,内存释放,内存泄漏一系列头疼的问题。
对了,还有移动构造可以考虑,移动可以省去拷贝。
假设我们抛开C++的历史包袱,现在只存在两个对象传递的方法,一个是复制,一个是移动[引用],那么问题就清楚了,对于分配在栈上的变量,例如int,char啥的我们可以复制,复制很廉价,对于分配在堆上的对象,我们可以移动,只需要保留好移动对象的引用即可,因此这里把移动和引用当做一个方案。
// 我们的新方案
int a = 0;
int b = a; // 对分配在栈上的类型拷贝,拷贝很廉价
Class C { /* sth. */ };
C *c1 = new C(); // 分配在堆上
C *c2 = c1; // 对分配在堆上的类型,采用移动
/*注意,移动以后,c1的内容就没了,不能再对c1访问,c2掌握了所有权,现在只有一个C类型的对象*/
// 离开作用域自动释放资源
有点像使用了C++的unique_ptr,不过简化了写法。这样有什么好处呢?不会出现两个指针访问同一个资源的情况,离开作用域自动释放资源,我们就不用操心资源释放的问题了,没有野指针,不会出现指针悬挂,就像unique_ptr做的一样。似乎只是个语法糖,我们再加一点就和基本的Rust所有权差不多了,那就是每个资源只有一个拥有所有权的引用。
那我们正式进入Rust(这里我们暂时不考虑Rust的智能指针):
let x = 0;
let y = x; // 第一种方式,拷贝
let s1 = String::from("hello");
let s2 = s1; // 第二种方式,移动
看起来这个逻辑很简单,但是马上我们会遇到一个问题,既然对于堆上的对象是移动,那么函数传递参数怎么办?
fn main() {
let s = String::from("hello");
takes_ownership(s);
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
}
我们不用管语法细节,代码很容易看懂,这里s传入函数,意味着s就废了,怎么办?如果你需要复制可以使用克隆:
let s1 = String::from("hello");
let s2 = s1.clone();
但是这不是我们要做的,我们希望函数做了操作,我们依旧可以对s继续操作,因此这里就需要引入所有权借用:
fn main() {
let s = String::from("hello");
takes_ownership(&s);
}
fn takes_ownership(some_string: &String) {
println!("{}", some_string);
}
但是我们还没有说一个特性,就是默认情况下变量是不可以变的,所以应该称为常量:
fn main() {
let x = 5;
x = 6;
}
这样做是报错的,你需要加入mut:
fn main() {
let mut x = 5;
x = 6;
}
同样,函数是这样写的:
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
不可变有什么好处呢,不可变是安全的,不可变不存在不确定,尤其是并发等情况下,如纯函数式语言就是不可变的。需要时我们让变量可变,控制变化因素,有助于减少潜在的问题。
还要一些问题需要讨论,下一篇文章见。