什么是所有权?
Rust的核心特性就是所有权。
所有程序在运行时都必须管理他们使用计算机内存的方式。通常的语言存在以下两种情况。
- 带有垃圾回收机制例如Java、C#,在程序运行时,垃圾回收器会不断地寻找不再使用的内存。
- 程序员显示地分配和释放内存,例如C/C++语言。
Rust语言采取了第三种方式,内存通过一个所有权系统来管理,其中包含一组编译器在编译时检查的规则。当程序运行时,所有权特性不会减慢程序的运行速度(因为Rust把内存管理相关的工作提前到编译时)。
Stack (栈内存) vs Heap (堆内存)
在像Rust这样的系统级编程语言里,一个值在stack上还是heap上对语言的行为和你为什么要做某些决定时存在更大影响的。
存储数据
- Stack (后进先出 LIFO Last in first out)
- 栈上添加数据为压入栈
- 栈上移除数据为弹出栈
- 所有存储在Stack上的数据必须拥有已知的固定的大小
- 编译时大小未知的数据或者运行时可能发生变化的数据必须存放在heap上
- Heap对于内存的组织性差一些
- 当把数据放入到heap时,会请求一定数量的空间
- 操作系统在heap里找到一块足够大的空间,把它标记为在用,并返回一个指针,也就是这个空间的地址
- 这个过程就叫做在heap上分配
把值push到stack上不叫分配,因为指针ptr是已知固定大小的,可以把指针存放在stack上。如果你想访问实际的数据,你必须使用指针来定位
The size of a pointer is not fixed. It depends upon different issues like operating system, CPU architecture etc. Usually it depends upon the word size of underlying processor for example a 32 bit computer the pointer size can be 4 bytes. For a 64bit computer the pointer size can be 8bytes. So for a specific architecture pointer size will be fixed.
把数据压入到stack上要比heap上分配快得多, 因为操作系统不需要寻找用来存储新数据的空间,那个位置永远都在stack的顶端,在heap上分配空间需要做更多的工作,操作系统首先需要找到一个足够大的空间来存放数据,然后做好记录方便下次分配
访问数据
访问heap中的数据要比访问stack中的数据慢,因为需要通过指针才能找到heap中的数据,对于现代的处理来说,因为存在缓存的缘故,如果指令在内存中跳转的次数越少,那么速度越快。
-
如果数据存放的距离比较近,那么处理器的处理速度就会更快一些(stack上)。
-
如果数据之间的距离比较远,那么处理速度就会慢一些(heap上)
- 在heap上分配大量空间也是非常耗时的
函数调用
当你的代码调用函数时,值被传入到函数(也包括指向heap的指针)。函数本地的变量会被压入到stack上。当函数结束后,这些值会从stack上弹出
所有权存在原因
-
所有权解决的问题
-
跟踪代码的哪些部分正在使用heap的哪些数据
-
最小化heap上的重复数据量
-
清理heap上未使用的数据以避免空间不足
-
所有权规则
- 每一个值都有一个变量,该变量时该值的所有者
- 每个值同时只能有一个所有者
- 当所有者超出作用域(scope)时,该值会被删除。
变量作用域
Scope就是程序中一个项目的有效范围,例如
fn main() {
// s 不可用
let s = "Hello";// s可用
// 可以对s进行相关操作
}// s作用域到此结束,s不再可用
以复杂的String类型讲所有权
String类型(存储在堆上的)比基础标量数据类型更复杂,来看看Rust是如何处理它的
创建String类型的值
可以使用from函数从字符串字面值创建出String类型,例如let s = String::from("Hello");
fn main() {
let mut s = String::from("Hello"); // mut(即mutable)代表 s是可以修改的
s.push_str(", World");
println!("{}", s);
}
内存和分配
- 字符串字面值,在编译时就知道它的内容了,其文本内容直接被硬编码到最终的可执行文件里,速度快、高效(得益于其不可变性)
- String类型,为了支持可变性,需要在heap上分配内存来保存编译时未知的文本内容
- 操作系统必须在运行时请求内存
- 通过调用String::from来实现
- 当用完String之后,需要使用某种方式将内存返回给操作系统
- 在拥有GC的语言中,GC会跟踪并清理不再使用的内存
- 没有GC,就需要我们去识别内存何时不再使用,并调用代码将它返回。
- 如果忘记了,那么将会浪费内存
- 如果提前,变量就会非法
- 如果做了两次则会引起Bug。必须一次分配对应一次释放
- 操作系统必须在运行时请求内存
Rust采用了不同的方式:对于某个值来说,当拥有它的变量走出作用范围时,Rust会调用一个特殊的函数drop,内存会立即自动交还给操作系统。
变量和数据交互的方式: 移动(Move)
一个String由三部分组成:
- 一个指向存放字符串内容的指针
- 一个长度len
- 一个容量capacity
fn main() {
let s1 = String::from("Hello");
let s2 = s1;
prinln!("{}", s1); // 会报错, borrow of moved value: 's1',
// move occurs because `s1` has type 'std::string::String', which does not implement the 'Copy' trait
}
为了保证内存的安全,在上面的代码中,当s1移动到s2后,Rust会让s1失效。
Rust中一个设计原则: Rust不会自动创建数据的深拷贝。
变量和数据交互的方式: 克隆(Clone)
如果真想对heap上面的String数据进行深度拷贝,而不仅仅是stack上的数据,可以使用clone方法。
fn main() {
let s1 = String::from("Hello");
let s2 = s1.clone();
println!("{}, {}", s1, s2);
}
克隆方法相当于是将stack和heap上的数据都完整拷贝了一份,clone这种方法是比较消耗资源的。
Stack上的数据: 复制
fn main() {
let x = 5;
let y = x;
println!("{}, {}", x, y); // x是整型,为标量类型,在编译的时就确定了大小
}
Copy trait,可以用于像整数这样完全存放在stack上面的类型,如果一个类型实现了Copy这个trait,那么旧的变量在赋值后仍然可用,如果一个类型或者该类型的一部分实现了Drop trait,那么Rust不允许让它再去实现Copy trait了
一些拥有Copy Trait的类型
- 任何简单标量的组合类型都可以是Copy的
- 任何需要分配内存或者某种资源的都不可以实现Copy Trait的
- 拥有Copy Trait的类型:
- 所有的整数类型,例如u32
- bool
- char
- 所有浮点类型,例如f64
- Tuple(元组),如果所有字段都是Copy的则是可以拥有Copy Trait的,例如(i32, i32)