Learning Rust - 2 所有权
所有权
1 定义
所有权(ownership)
是Rust
用于如何管理内存的一组规则。所有程序都必须管理其运行时使用计算机内存的方式。
一些语言中具有垃圾回收机制,在程序运行时有规律地寻找不再使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。
Rust
则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。如果违反了任何这些规则,程序都不能编译。在运行时,所有权系统的任何功能都不会减慢程序。
关于栈与堆的概念这里不再详解,在此有非常详细与形象的说明,读者自行学习。
访问栈数据的速度是更快的,但是会有一定的限制,所以合理的分配方法对于提升程序效率来说,是非常重要的。
https://kaisery.github.io/trpl-zh-cn/ch04-01-what-is-ownership.html
所有权遵循以下的规则:
- 每一个值都有一个
所有者(owner)
。 - 每一个值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
2 作用域
作用域是一个项(item)在程序中有效的范围。
假设有这样一个变量:
fn main(){ // s 在这里无效,它尚未声明 |
变量与数据的交互方式具有以下两种方式:移动(move)与克隆(clone)
移动
例2.1:fn main(){
let x = 5;
let y = x;
}
对于简单的数据类型来说,其值已知切固定,编译器可以将其硬编码,因此此时会将x
,y
都加入到栈中,也就是调用x
,y
有效。
实际上,对于以下的数据类型,都满足此操作,被称为Copy
的,或者称为实现了Copy trait
。
- 所有整数类型,比如 u32。
- 布尔类型,bool,它的值是 true 和 false。
- 所有浮点数类型,比如 f64。
- 字符类型,char。
- 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有。
而对于String
类型来说,情况有所不同:
例2.2:fn main(){
let s1 = String::from("hello");
let s2 = s1;
}
让我们详细了解这个过程中发生了什么。首先对于s1
来说,实际上在进行这样的操作:
将实际的字符串数据存放在堆上,具有指向存放字符串内容内存的指针,长度和容量大一组数据存储在栈上。如图2.1所示。
Figure-2.1 字符串赋值(图片来源Rust程序设计语言)
当我们将s1
赋值给s2
,String
的数据被复制了,这意味着从栈上拷贝了它的指针、长度和容量,并没有复制指针指向的堆上数据。
Figure-2.2 s1赋值于s2(图片来源Rust程序设计语言)
但是这就存在一个问题:当s2
和s1
离开作用域,它们都会尝试释放相同的内存。这是一个叫做二次释放(double free)
的错误,也是之前提到过的内存安全性bug之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。
因此,对于下列的代码,将会报错:
例2.3:fn main(){
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world!");
}
--> src/main.rs:5:15 |
因此,在let s2 = s1;
之后,Rust
认为s1
不再有效,也无需维护其回收的任务,这种操作叫做移动(move)
。
克隆
如果确实需要复制String
堆上的数据,则可以使用clone()
这个通用函数。
例2.4:fn main(){
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{s1}, world!");
println!("{s2}, world!");
}
此时可以正常执行。
3 所有权与函数
向函数传递参数的时候可能会发生克隆或者移动,如下例所示:
例3.1:fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值移动到函数里,这里不再有效
let x = 5; // x 进入作用域
makes_copy(x); // x 应该移动函数里,但 i32 是 Copy 的,所以在后面可继续使用 x
} // 这里,x 先移出了作用域,然后是 s。但因为 s 的值已被移走,没有特殊之处
fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{some_string}");
} // 这里,some_string 移出作用域并调用 `drop` 方法,占用的内存被释放
fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{some_integer}");
} // 这里,some_integer 移出作用域。没有特殊之处
函数返回值可以转移所有权,如下例所示:
例3.2:fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值,转移给 s1
let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2); // s2 被移动到takes_and_gives_back 中,也将返回值移给 s3
} // 这里,s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,所以什么也不会发生。s1 离开作用域并被丢弃
fn gives_ownership() -> String { // gives_ownership 会将返回值移动给调用它的函数
let some_string = String::from("yours"); // some_string 进入作用域。
some_string // 返回some_string,并移出给调用的函数
}
// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
a_string // 返回 a_string 并移出给调用的函数
}
4 引用(reference)
若要使用一个函数处理String
,处理完后仍然需要使用这个值,可以使用引用的方式进行:
例4.1:fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize { // s 是 String 的引用
s.len()
}// 在这里,s 离开了作用域。但因为它并不拥有引用值的所有权,所以什么也不会发生
这段代码中,&
符号就是引用,允许使用值而不获取所有权。
Figure-4.1 引用示意图(图片来源Rust程序设计语言)
我们将创建一个引用的行为称为借用(borrowing)
。
引用分为可变引用于不可变引用,默认为不可变引用,创建可变引用只需要在&
符号后加上mut
即可:
例4.2:fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
println!("{}", r1);
}
非常严肃的问题是,对于可变引用,需要遵守以下的规定:
- 不可以同时对一个变量实现多个可变引用
- 不可以同时对一个变量实现可变引用与不可变引用
- 允许同时对一个变量实现多个不可变引用
还有一个悬垂引用(Dangling References)
的概念,也就是通过释放内存导致指向其的指针生成一个错误的悬垂指针(Dangling Pointers)
。
例4.3:fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s // 返回字符串 s 的引用
}// 由于所有权还在函数中,这里 s 离开作用域并被丢弃。其内存被释放,导致返回指针丢失。
Rust
编译器将会检查悬垂引用的错误。
5 Slice类型引用
slice
允许引用集合中一段连续的元素序列,而不用引用整个集合,它没有所有权。
fn main(){ |
&str
类型实际上就是一个指向二进制程序特定位置的slice
类型,这就是为什么&str
是一个不可变引用