rust学习(一)
所有权
介绍
Rust 使用所有权机制来管理内存,实际上是一组规则。这会与其他语言不同,一些语言使用垃圾回收机制来管理内存,如 Java 以及运行在 JVM 下的其他语言;还有一些需要手动释放内存,例如 C++等。与这些语言都不同,Rust 使用自己新的一套机制(所有权)来管理内存,编译时,会检查是否通过这些规则,而运行时并不会影响运行效率。
所有权规则
- Rust 中的每一个值都有一个 所有者(owner)。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
变量的作用域
根据所有权的规则,当一个值离开作用域就无效。例如,使用官网里的例子
1 | let s = "hello1"; |
hello2
在进入代码块中的作用域时生效,掩盖原来的hello1
,在离开代码块后,失效。
总之:
- 当 s 进入作用域 时,它就是有效的。
- 这一直持续到它 离开作用域 为止。
而 Rust 的作用域范围与其他语言一样。
String 类型介绍
Rust 的 String 类型不可变,这点和 Java 类似。它也可以存储编译时未知的数据,例如用户输入等。
创建
可以使用from
方法创建
1 | let s = String::from("hello"); |
也可以这样
1 | let mut s = String::from("hello"); |
可是为什么 string 是不可变的,却在第二种情况下可以“改变”内容(String
可变,字面值不可变)呢?
内存分配
要支持String
可变,及维护一个内存长度未知的内存空间,那就一定要在堆上分配一块在编译时未知大小的内存空间。
也就是说
- 必须在运行时向内存分配器(memory allocator) 请求内存
- 需要处理完
String
时将内存返回给分配器的方法。
第一种方式创建,每种语言的实现方式都差不多,即String
的实现(implementation)请求所需要的内存(编译时就知道了)。
第二种方式就不太一样了:
- 有 GC 的语言中,GC 会识别并清理不再使用的内存;
- 没有 GC 的语言中,我们需要手动释放内存
- Rust 与前两者都不同,而是采取了离开作用域就释放的机制(当变量离开作用域时,Rust 会自动调用一个特殊函数
drop
)
变量与数据交互的方式(一):移动
有下述例子:
1 | let x = 5; |
这段代码做了以下的事情,将5
绑定到 x;拷贝x
的值并绑定到y
。因为5
这个值是固定的值,所以这两个变量就直接存到栈中。
第二个例子:
1 | let s1 = String::from("hello"); |
这段代码与上段代码不太一样,这里s2
并不是拷贝并绑定的操作。
我们关注String
内部结构,左侧存在栈上,右侧存在堆上。
当我们将s1
赋值给s2
,String 上的内容会被复制,意味着我们拷贝了它栈上的内容,但并没有拷贝堆上的内容。
这个拷贝也就是浅拷贝。
但是这里就出现了一个问题,上述我们说到,当变量离开作用域时,会执行drop
函数,释放内存,于是s1
与s2
就会都释放一边。因为他们都指向同一片堆区的内容,因此会出现二次释放(double free),这就会造成内存安全问题。
那 Rust 是怎么解决这个问题的呢,试着运行下述代码:
1 | let s1 = String::from("hello"); |
会出现报错
1 | error[E0382]: borrow of moved value: `s1` |
由报错3 | let s2 = s1; | -- value moved here
可以看出,Rust 将s1
在拷贝的时候删除了,即直接将s1
失效,我们把这个过程称为移动。
变量与数据交互的方式(二):克隆
说完浅拷贝(在栈上拷贝),那必不可少的就是深拷贝(还要拷贝堆上的内容)。
Rust 使用函数clone
执行深拷贝的操作。
1 | let s1 = String::from("hello"); |
不过这里还有一点要注意,浅拷贝拷贝栈上的内容,如果像基本类型的数据,直接赋值是不会触发移动的。
1 | let s1 = 5; |
这里不会出现报错
1 | Compiling hello_world v0.1.0 (D:\projects\rust\hello_world) |
Rust 内部有个一个特殊注解Copy
trait,实现这样的功能(就变量赋值后能继续用)。
Rust 不允许实现Drop
trait 的类型使用Copy
trait。也就是说,当一个值离开作用域时,我们不能添加Copy
trait
实现Copy
trait 的类型:
- 整形、布尔型、浮点型、字符
- 元组,当且仅当元组中也都实现了
Copy
trait
所有权与函数
传入函数的过程与赋值语句差不多,会出现移动和赋值的情况。
我这里直接给出官网的实例
1 | fn main() { |
注意s
传入函数之后,会触发移动,导致s
失效。
返回值与作用域
返回值也会触发所有权的机制。
同样给出官网实例代码
1 | fn main() { |
这时出现一个问题,频繁地转移所有权会很麻烦,我们需要一个操作,不拿到其所有权,但是可以使用它。这就是引用(references)。