什么是所有权
Rust的所有权,是一个跨时代的理念,是内存管理的第二次革命。
较低级的语言依赖程序员分配和释放内存,一不小心就会出现空指针、野指针破坏内存;较高级的语言使用垃圾回收的机制管理内存,在程序运行时不断地寻找不再使用的内存,虽然安全,却加重了程序的负担;Rust的所有权理念横空出世,通过所有权系统管理内存, 编译器在编译时会根据一系列的规则进行检查,在运行时,所有权系统的任何功能都不会减慢程序,把安全的内存管理推向了0开销的新时代。
Rust的所有权并不难理解,它有且只有如下三条规则:
- Rust中的每一个值都有一个所有者(owner)的变量;
- 一个值有且只有一个所有者;
- 当所有者离开作用域时这个值将被丢弃。
真正难以理解的是内存的使用情况。
栈和堆的回忆
在学习C/C++时,老师经常出某某变量被分配在栈上还是堆上的题目,几乎每次都有很大一批同学在这种题目上失手。
栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同。栈是一种后进先出(LIFO)的数据结构,栈中的所有数据都必须占用已知且固定的大小。堆就好理解了,它是一个没有组织的结构,你想怎么使用就怎么使用,只要堆够大,你就可以申请一段内存空间,然后把这段内存标记为已使用,并得到指向这段内存开头的指针;当你不再使用时,再将这段内存标记回未使用。
当声明了一个指针但并没有分配空间时,这个指针是空指针;当内存已经标记回未使用,而指针还依然指向这段空间时,这个指针就是野指针。
回到刚才说的题目,其实分辨起来很容易,动态分配的变量就是在堆上,其他的都在栈上。
栈是一个成熟的结构,基本不会引发内存的问题,而没有组织的堆却很容易引发内存问题。垃圾回收和所有权,都是为了解决堆的内存管理问题。
拷贝(Copy)、移动(Move)与克隆(Clone)
Rust把栈上的内存复制,称为拷贝(Copy)。看下面的代码:
fn main()
{
let x = 5;
let y = x;
println!("x:{} y:{}", x, y);
}
整数的内存是分配在栈上,Rust将x的值拷贝到的y变量中。
而对于字符串,看上去一样的代码却是不一样的结果:
fn main()
{
let x = String::from("hello");
let y = x;
println!("x:{} y:{}", x, y);
}
看似简单的代码却不能通过编译:
error[E0382]: borrow of moved value: `x`
--> src\main.rs:5:27
|
3 | let x = String::from("hello");
| - move occurs because `x` has type `std::string::String`, which does not implement the `Copy` trait
4 | let y = x;
| - value moved here
5 | println!("x:{} y:{}", x, y);
| ^ value borrowed here after move
error: aborting due to previous error
这里面提到了Copy
trait,暂且不管,大概意思是值从x移动到了y,移动之后,x不能用了。
为什么会这样呢?
Rust中String类型变量的内存是在堆上分配的,执行let x = String::from(“hello”)时,Rust在堆上分配了内存,并将其绑定到x变量。String类型有三个成员变量(先沿用C++的概念),一个是指向分配的内存的指针,一个是内存的长度,另一个是内存使用了的长度。
当执行let y=x时,Rust将x的这三个成员变量拷贝给了y,但是并没有将字符串占用的内存拷贝给y。这类似于其他语言的浅拷贝,只不过Rust还多做了一步,让原来的x失效。这在Rust中被称为移动(Move)。
要牢记下面一句话:Rust永远也不会自动创建数据的 “深拷贝”。
要想进行深拷贝,需要使用克隆(Clone):
fn main()
{
let x = String::from("hello");
let y = x.clone();
println!("x:{} y:{}", x, y);
}
这段代码就可以正常的运行了。
拷贝、移动和克隆,不仅仅发生在赋值时,当调用函数传递参数或从函数接收返回值时,它们也会发生,我自己写猜猜看游戏时,就是在把String当作参数传递到其他函数内时发生了移动。如下面的代码:
fn main()
{
let x = String::from("hello");
print(x);
println!("x:{}", x);
}
fn print(s:String)
{
println!("{}", s)
}
编译器会报错:
error[E0382]: borrow of moved value: `x`
--> src\main.rs:5:22
|
3 | let x = String::from("hello");
| - move occurs because `x` has type `std::string::String`, which does not implement the `Copy` trait
4 | print(x);
| - value moved here
5 | println!("x:{}", x);
| ^ value borrowed here after move
error: aborting due to previous error
调用print函数时,x的值移动到了print函数内,而print函数执行完成后,s离开了作用域,内存被销毁了。再使用x变量就会出现问题。
在其它语言中,比如Python,并不容易分清哪些是浅拷贝,哪些是深拷贝,而Rust则将其严格的区分开来,当你看到或用到clone时,一定要引用注意,这么做是耗费资源的,一定要这么做吗?
引用与借用
当移动发生时,内存的所有权发生了转移。那么有没有办法不转移所有权呢?答案是肯定的,就是通过引用。将我前面出错的例子修改为引用:
fn main()
{
let x = String::from("hello");
print(&x);
println!("x:{}", x);
}
fn print(s:&String)
{
println!("{}", s)
}
这个print函数的参数不再是字符串,而是字符串的引用,这样,内存的所有权依然是属于x,而不是s,当s离开作用域后,内存不会被销毁,x还可以继续使用。
Rust将获取引用作为函数的参数的方式叫做借用(borrowing),这很容易被理解,张三有5块钱,他把钱给李四了,钱的所有权从张三转移到李四,和张三没有关系了;而如果李四是向张三借钱,钱的所有权不会发生转移,还是张三的,只是李四用了一下而已。
引用也具有可变性,同样,Rust默认是不可变的。要想让引用可变,双方都需要添加mut关键字:
fn main()
{
let mut x = String::from("hello");
print(&mut x);
println!("x:{}", x);
}
fn print(s:&mut String)
{
s.push_str(" world.");
println!("{}", s)
}
从执行结果中可以看出,print函数中改变了字符串的值,同时,main函数中x的值也同样发生了改变,因为他们是同一段内存地址。
我能把钱同时借给两个人吗?可以,但同时只能有一个人花。Rust限制可变引用的数量,同时只能有一个可变引用:
fn main()
{
let x = String::from("hello");
let y = &x;
let z = &x;
println!("x:{} y:{} z:{}", x, y, z);
}
这是没有问题的,但换成可变引用:
fn main()
{
let mut x = String::from("hello");
let y = &mut x;
let z = &mut x;
println!("x:{} y:{} z:{}", x, y, z);
}
编译器就会报错:
error[E0499]: cannot borrow `x` as mutable more than once at a time
--> src\main.rs:5:13
|
4 | let y = &mut x;
| ------ first mutable borrow occurs here
5 | let z = &mut x;
| ^^^^^^ second mutable borrow occurs here
6 | println!("x:{} y:{} z:{}", x, y, z);
| - first borrow later used here
那好,既然不能同时有两个,那就去掉一个,改成:
fn main()
{
let mut x = String::from("hello");
let y = &x;
let z = &mut x;
println!("x:{} y:{} z:{}", x, y, z);
}
编译器还是会无情的亮红灯:
error[E0502]: cannot borrow `x` as mutable because it is also borrowed as immutable
--> src\main.rs:5:13
|
4 | let y = &x;
| -- immutable borrow occurs here
5 | let z = &mut x;
| ^^^^^^ mutable borrow occurs here
6 | println!("x:{} y:{} z:{}", x, y, z);
| - immutable borrow later used here
难道是只要有可变引用就不能再使用?也没有这么绝对,再看一面的代码:
fn main()
{
let mut x = String::from("hello");
let y = &x;
println!("x:{} y:{}", x, y);
let z = &mut x;
z.push_str(" world");
println!("z:{}", z);
}
怎么又没有问题了呢?Rust的编译器还是很智能的,只要不可变的引用在可变的引用之后不再使用,那么就不会出现数据竞争的问题,那么,自然就可以编译通过了。
悬垂引用
C/C++中,常见的指针错误还有野指针,即指针指向的内存已经被释放,继续使用该指针会引发错误。这在Rust中叫做悬垂引用(Dangling References)。当然,你不需要担心发生了悬垂引用怎么办,因为Rust的编译器会检查到悬垂引用:
fn main()
{
let x = get_string();
println!("x:{}", x);
}
fn get_string()->&String
{
&String::from("hello")
}
先来分析这段代码,main函数中调用了get_string函数获取一个字符串的引用,因为这个字符串是在get_string中创建的,函数结束时这个字符串的生命周期随之结束,引用返回给x后,就发生了悬垂引用。Rust的编译器会检查到这个错误:
error[E0106]: missing lifetime specifier
--> src\main.rs:7:18
|
7 | fn get_string()->&String
| ^ help: consider giving it a 'static lifetime: `&'static`
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
error: aborting due to previous error
与想像中的不同,错误并不是指出发生了悬垂引用,而是说缺少了生命周期说明。
Rust有一个独特的语法,叫做生命周期注解,这在其他语言中没有的。不过生命周期注解并不能解决上面的悬垂引用问题,Rust的编译器不会允许悬垂引用的发生。
生命周期注解大多数情况下都是与泛型结合使用,这里简单记录一下生命周期注解的语法,学习泛型的时候再详细记录。
生命周期参数名称以单引号开头, 其名称通常全是小写,如:
&i32 // 引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用
生命周期注解大多数情况下都是与泛型结合使用,这里简单记录一下生命周期注解的语法,学习泛型的时候再详细记录。
话说回来,上面的悬垂引用的问题到底如何解决呢?答案是不使用引用:
fn main()
{
let x = get_string();
println!("x:{}", x);
}
fn get_string()->String
{
String::from("hello")
}
get_string函数结束前,字符串的所有权被转移到x变量中,因此字符串的内存不会被销毁,可以继续被使用。
重新认识切片
在Rust基础概念一章中,已经记录了切片的使用方法,在理解了Rust的所有权以后,有必要重新认识一下切片了,因为,它是一个没有所有权的类型,其本质还是引用,只不过引用的是数据的一部分。
回顾一下切片的使用:
fn main() {
let a = [1,2,3,4,5,6,7,8,9,0];
let s = &a[2..8];
println!("{}", s[0]);
}
当时学习的时候有没有这样的疑问,为什么a[2…8]前面要有‘&’符号?现在应该能理解了吧。
对于字符串的切片,同样适用前面的引用规则,不过有点些特殊的地方:即便是对字符串不同区域的引用,也不能同时拥有两个可变引用。如:
fn main()
{
let mut x = String::from("hello world");
let frist = &mut x[..5];
let second = &mut x[6..];
println!("frist:{} second:{}", frist, second);
}
frist是"hello",second是"world",这两个切片是不重叠的。但编译器还是会报错:
error[E0499]: cannot borrow `x` as mutable more than once at a time
--> src\main.rs:5:23
|
4 | let frist = &mut x[..5];
| - first mutable borrow occurs here
5 | let second = &mut x[6..];
| ^ second mutable borrow occurs here
6 | println!("frist:{} second:{}", frist, second);
| ----- first borrow later used here
error: aborting due to previous error
在Rust基础概念记录字符串时,我写到:
在Rust中,字符串被分为两种,str和String
特此更正一下,应该是String和&str,原文已经进行了修正,希望没有对朋友们造成误导。
&str就是字符串的字面值,事实上,它就是一个切片,是对字符串的所有字符引用的切片。
这个字符串编译完后保存在可执行文件中,执行时其内存在代码区,既不在栈里,也不在堆里。因此,它是不可变的。要想操作它,需要先将其转换为String,String的内存保存在堆里,就可以任意操作了。
回到前面print函数的例子:
fn main()
{
let x = String::from("hello");
print(&x);
}
fn print(s:&String)
{
println!("{}", s)
}
将String类型的引用作为函数的参数,它可以正常工作,但是,如果传入的是字符串字面值:
fn main()
{
print("hello");
}
fn print(s:&String)
{
println!("{}", s)
}
严格的编译器又开始啰嗦了:
error[E0308]: mismatched types
--> src\main.rs:3:11
|
3 | print("hello");
| ^^^^^^^ expected struct `std::string::String`, found str
|
= note: expected type `&std::string::String`
found type `&'static str`
error: aborting due to previous error
此时,我们需要传入的参数类型应该是&str:
fn main()
{
print("hello");
}
fn print(s:&str)
{
println!("{}", s)
}
确实,它工作的很好,那我再传入String会怎么样?
fn main()
{
print("hello");
let x = String::from("world");
print(&x);
}
fn print(s:&str)
{
println!("{}", s)
}
什么?它竟然还能工作?这又是为什么?
上面代码中的print(&x),是对整个字符串的引用,它与print(&x[…]);是等价的,因此,在需要将字符串作为函数参数时,最好的方式是用&str作为参数,这样可以通用。如果函数中需要对字符串进行其它操作,可以使用to_string()将参数转换为String类型进行处理。