详细解释Rust的所有权和borrow规则
Rust的所有权系统使得Rust无需GC即可保证内存安全,它影响整个Rust,也使得Rust的很多编码方式和其他语言不太一样。
Rust的栈和堆
栈中的所有数据都必须占用已知且固定的大小,而那些在编译时大小未知或大小可能变化的数据,需存储在堆上。
那些大小固定的基本数据类型(int、float、bool、char、tuple、array)都存储在栈上,此外,指针的大小是已知且固定的,因此指针也存储在栈上。
堆是缺乏组织的:向堆放入数据时,要请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针(pointer)。这个过程称作「在堆上分配内存」(allocating on the heap),有时简称为「分配」(allocating)。将数据推入栈中并不被认为是分配。
入栈比在堆上分配内存要快,因为入栈时操作系统无需为存储新数据去搜索内存空间,其位置总是在栈顶。在堆上分配内存则需要更多的工作,因为操作系统必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备,因此在堆上分配大量的空间会比较消耗时间。
访问堆上的数据也比访问栈上的数据慢,因为必须通过栈上的指针来访问其指向的堆内数据,这比访问栈中数据多一次内存跳转。
当调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中,当函数结束时,这些值被移出栈。
Rust的所有权系统用于管理堆数据。例如,String::from("hello")
创建的String类型的字符串就是保存在堆中的,在创建这个字符串时,需要请求操作系统分配堆内存,然后返回这个堆内存的地址,当不再使用这个字符串时,也就是回收这个字符串时,则会用到所有权系统。
变量作用域
Rust的所有权系统和作用域息息相关。
在Rust中,可以单独使用一个大括号来开启一个作用域:
{ // s 在这里无效, 它尚未声明
let s = "hello"; // 从此处起,s是有效的
// 使用 s
} // 此作用域已结束,s 不再有效
上面的代码中,变量s绑定了字符串字面值,在跳出作用域后,变量s和内存中的字符串字面值都被销毁。
Rust所有权规则概述
所有权规则如下:
- Rust中的每个值都有一个被称为其所有者(owner)的变量(注:值的所有者是某个变量)
- 值在任一时刻有且只有一个所有者
- 当所有者(变量)离开作用域,这个值将被丢弃
- 当所有者离开作用域时,将调用一个特殊的名为drop的函数,该函数内部用于放置释放内存的代码
例如:
fn main(){
{
let mut s = String::from("hello"); // 堆中字符串的owner是s
} // 跳出作用域,s离开作用域,s被销毁,堆中的字符串也被回收
}
Rust中变量的移动
在其他语言中,有深拷贝和浅拷贝的概念,浅拷贝描述的是只拷贝数据对象的引用,深拷贝描述的是拷贝堆内存中的数据。
在Rust中没有深浅拷贝的概念,但有移动和复制(拷贝、克隆)的概念。而且,Rust绝不会自动拷贝堆内存中的数据(但可以手动拷贝)。
「栈中的数据只拷贝,不移动」。
对于如下代码:
fn main(){
// s1绑定了栈中的3(因为3是大小固定的整型数据)
let s1 = 3;
let s2 = s1; // 拷贝s1的值给s2,现在栈中有两个3
println!("{},{}", s1, s2);
}
变量s1绑定了栈中的数据3,然后拷贝了s1并赋值给s2,因为s1绑定的是栈中数据,所以拷贝s1其实是拷贝了栈中的3,使得栈中有两个3。
再看下面堆中数据的赋值。
fn main(){
let s1 = String::from("hello");
let s2 = s1;
// 将报错error: borrow of moved value: `s1`
println!("{},{}", s1, s2);
}
变量s1绑定的是堆中数据(String类型的hello字符串),此时该数据的所有者是s1。
当执行let s2 = s1
时,将不会拷贝堆中数据赋值给s2,也不会像其他语言一样让变量s2也绑定堆中数据。
因此,下图的内存引用方式不适用于Rust(注:String类型字符串存储在堆中,但其变量s1存储在栈中,s1是一个胖指针结构,该结构包含一个指向堆中数据的指针,一个表示数据当前大小的长度,以及一个表示操作系统为该数据所分配的内存空间大小)。
如果Rust采用这种内存引用方式,按照Rust的所有权规则,变量在跳出作用域后就销毁堆中对应数据,那么在s1和s2离开作用域时会导致「二次释放」同一段堆内存,这会导致内存污染。
于是Rust采用更直接的方式,当执行let s2 = s1
时,直接让s1无效,而是只让s2绑定堆内存的数据。也就是将「s1移动到s2」,也称为「堆数据的所有权从s1移给s2」。
Rust堆内存数据的拷贝
虽然Rust绝不会自动拷贝堆内存中的数据,但可以使用clone()
方法手动拷贝它们。
fn main(){
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{},{}", s1, s2);
}
要注意,如果拷贝的堆内存数据较大,这会严重影响性能。但如果要拷贝的堆数据不大,可以不用顾虑拷贝堆内存带来的效率问题。
Rust栈中的数据拷贝
对于前面的示例:
fn main(){
let s1 = 3;
let s2 = s1;
println!("{},{}", s1, s2);
}
上面的代码不会报错,因为3是整型数据,它保存在栈中而不是堆中。因为栈中数据大小是固定的,拷贝起来速度很快,所以没有必要在拷贝时让s1无效。
也就是说,对于栈中数据,调用clone()方法和直接拷贝变量的效果是一致的。
实际上,上面s1之所以仍然有效,是因为整型数据拥有Copy trait。
当某类型拥有Copy trait时,将旧的变量赋值给新变量后仍然可用。
那么什么类型是Copy的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则,任何简单标量值的组合可以是Copy的,不需要分配内存或某种形式资源的类型是Copy的。如下是一些Copy的类型:
- 所有整数类型,比如u32
- 所有浮点数类型,比如f64
- 布尔类型,bool,它的值是true和false
- 字符类型,char
- 元组,当且仅当其包含的类型也都是Copy的时候。比如(i32, i32)是Copy的,但(i32, String)不是
函数参数和返回值的所有权移动
函数参数类似于变量赋值,在调用函数时,会将所有权移动给函数参数。
函数返回时,返回值的所有权从函数内移动到函数外变量。
例如:
fn main(){
let s1 = String::from("hello");
// 所有权从s1移动到f1的参数
// 然后f1返回值的所有权移动给s2
let s2 = f1(s1);
println!("{}", s2); // 注意,println!()不会转移参数所有权
let x = 4; // x绑定的是栈中数据(是Copy的)
f2(x); // 没有移动所有权,而是拷贝一份给f2参数
} // 首先x跳出作用域,其对应栈数据出栈,
// 然后s2跳出作用域,并释放对应堆内存数据,
// 最后s1跳出作用域,s1没有所有权,所以没有任何其他影响
fn f1(s: String) -> String {
let ss = String::from("world");
println!("{},{}", s,ss);
s // s绑定的堆数据所有权移动到函数外
} // ss跳出作用域,其对应堆中数据被释放
fn f2(i: i32){
println!("{}",i);
} // i跳出作用域,i对应栈数据出栈
很多时候,变量传参之后丢失所有权是非常不方便的,这意味着函数调用之后变量就不可用了。为了解决这个问题,可以将变量的「引用」传递给参数,这样不会丢失所有权。
引用和所有权借用
所有权不仅可以转移(原变量会丢失所有权),还可以借用所有权(borrow ownership)。
使用变量var的引用,即可借用变量var的所有权,借完之后会自动交还所有权给var,从而使得原变量var不丢失所有权。
例如:
fn main(){
{
let s = String::from("hello");
let sf1 = &s;
let sf2 = &s;
println!("{},{},{}",s,sf1,sf2); // 实际上这里也借用了s的所有权
// 使用完成后,这里就开始交还所有权
} // sf2离开,sf1离开,s离开
}
也可以将变量的引用传递给函数的参数,从而保证在调用函数时变量不会丢失所有权。
fn main(){
let s = String::from("hello");
let s1 = s.clone();
f1(s1); // s1丢失所有权,s1将因此而效
// println!("{}", s1);
let l = f2(&s); // 传递s的(不可变)引用,借用s所有权
// 交还s所有权
println!("{} size: {}", s, l); // s仍然可用
}
fn f1(s: String){
println!("{}", s);
}
fn f2(s: &String)->usize{
s.len() // len()返回值类型是usize
}
可变引用和不可变引用的所有权规则
变量的引用分为「可变引用&mut var
和不可变引用&var
」。
- 不可变引用:相当于只读引用(借用只读权),不允许修改其引用变量的堆数据
- 可变引用:相当于可写引用(借用可写权),允许修改其引用变量的堆数据
- 多个不可变引用可共存(可同时读)
- 可变引用具有排他性,不允许多个可变引用共存,也不允许可变引用和不可变引用共存(写时不允许读也不允许其他写)
- 得益于可变引用的排他性,直接避免了可能存在的同时修改数据导致的数据不同步问题,也避免了读取过期数据的问题
前面示例中f2(&s)
传递给函数参数的是变量s的不可变引用&s
,即借用了s的只读权,因此无法在函数内部修改该引用的堆数据值。
如要允许通过变量var的引用去修改堆数据值,要求:
- var是可变的,即
let mut var = xxx
- var的引用,要求引用可写的var,即
let varf = &mut var
例如:
fn main(){
let mut s = String::from("hello");
f1(&mut s); // 将s的可写权借给函数f1的参数
// f1退出后,交还所有权给s
println!("{}", s);
}
fn f1(s: &mut String){
s.push_str("world");
}
使用可变引用时,一定要注意它的排他性:可变引用和可变引用不可共存,可变引用和不可变引用也不可共存。
例如:
fn main(){
let mut s = String::from("hello");
let sf = &mut s; // 可变引用,可通过sf修改堆数据
sf.push_str("world");
// 下面报错
// let sf1 = &s; // 可变引用和不可变引用不可共存
}
另外,「创建可变引用后,在使用可变引用之前不允许使用原变量」(不能访问也不能修改原变量)。本示例之后将给出更严谨的说法。
fn main() {
// 正确
let mut s = String::from("hello");
let sf = &mut s;
sf.push_str("world");
println!("{}", sf); // 这里先使用了可变引用sf,后面可以继续使用原变量s
println!("{}", s);
// 错误
let mut ss = String::from("hello");
let ssf = &mut ss;
ssf.push_str("world");
ss.push_str("world"); // 后面代码使用了可变引用,这里不能使用ss
println!("{}", ss); // 后面代码使用了可变引用,这里不能使用ss
println!("{}", ssf); // 这里使用了可变引用,前面代码就不能使用原变量ss
// 删除本行,前面两行使用ss的代码可编译成功
// 这也是错的
let mut sss = String::from("hello");
let sssf = &mut sss;
sssf.push_str("world");
println!("{},{}", sss,sssf); // 不允许同时使用原始变量和不可变引用
// 这也是错的
let mut ssss = String::from("hello");
ssss.push_str("HELLO"); // 存在可变引用之前,可使用原始变量
let ssssf = &mut ssss;
ssssf.push_str("world");
ssss.push_str("WORLD"); // 不允许在使用可变引用前使用(访问或修改)原始变量
println!("{}", ssssf); // 删除本行,上一行将编译通过
}
所有权的作用域
更严谨的说法是,「引用的所有权作用域是从声明的地方开始一直持续到最后一次使用为止,而不是由何时跳出作用域决定」。这很容易理解,只要不再使用,就可以立即交还已借用的所有权。
因此,「只要可变引用的所有权作用域没有和不可变引用或原始变量的所有权作用域出现交叉重叠」,就可以同时使用它们。
例如:
fn main() {
let mut s = String::from("hello"); //// |let sf = &s; //// (2) |println!("{},{}",s,sf);//// (1)let ssf = &mut s;//// (3) |println!("{}", ssf);//// |println!("{}", s);//
}
原变量s的所有权作用域是(1),只读引用sf的所有权作用域是(2),可写引用ssf的所有权作用域是(3)。
可写引用的所有权作用域(3)内,不能使用原变量和其他引用。因此,(3)的内部不允许使用sf和s。
悬垂引用
在支持指针操作的语言中,一不小心就会因为释放内存而导致指向该数据的指针变成悬垂指针(dangling pointer)。
Rust的编译器保证永远不会出现悬垂引用:「引用必须总是有效」。即引用必须在释放数据之前丢弃,而不能先释放数据的内存而继续持有引用。
例如,下面的代码不会通过编译:
fn main(){
let sf = f(); // sf是一个无效引用
}
fn f() -> &String {
let s = String::from("hello");
&s // 返回s的引用
} // s跳出作用域,堆中String字符串被释放
编译时将报错:
error[E0106]: missing lifetime specifier
--> src\main.rs:6:11
|
6 | fn f() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
因此,可让函数直接返回String而非它的引用。
fn main(){
let sf = f(); // sf是一个无效引用
}
fn f() -> String {
let s = String::from("hello");
s // 返回s的引用
} // s跳出作用域,堆中String字符串被释放