所有权系统的设计目标:跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间
所有权规则
- Rust 中的每一个值都有一个被称为其 所有者(owner)的变量。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
首先:
字符串字面值,属于硬编码,即编译期已经编译进代码段,不会随着程序运行而改变,至于其具体类型,Rust也没说。
let s = "hello";
普通变量的生命周期和作用域和c++类似。遗憾的是这里的s不是普通变量,ta是个slices类型,天生的引用类型,引用的生命周期和c++不一样,所有权章节会遇到。
String类型作为复杂类型的代表,何谓复杂类型,理解上存在非POD类型的数据,或者从c++语义上理解就是需要运行时做额外操作的,比如分配内存,比如初始化虚表。
{
let s = String::from("hello"); // 从此处起,s 是有效的
// 使用 s
} // 此作用域已结束,
// s 不再有效
Rust在这一点上像把所有变量变成了unique_ptr对象,只在定义的局部有效,超过作用域就要自动被释放。
当 s
离开作用域的时候。当变量离开作用域,Rust 为我们调用一个特殊的函数。这个函数叫做 drop
,在这里 String
的作者可以放置释放内存的代码。Rust 在结尾的 }
处自动调用 drop
。这点就像C++的栈上对象离开作用域自动调用析构函数。
变量与数据交互的方式(一):移动
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); //编译错误
error[E0382]: use of moved value: `s1`
--> src/main.rs:5:28
|
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}, world!", s1);
| ^^ value used here after move
|
= note: move occurs because `s1` has type `std::string::String`, which does
not implement the `Copy` trait
其实理解上就是:Rust 永远也不会"自动"创建数据的 “深拷贝”,为此设计了一种新的语义,即一旦需要拷贝,那么,拷贝的源变量将会被标记为一个无效变量,不可再使用,当然也不需要再释放(drop),而其所有权转移到了新的拷贝目标变量上。
这一点,其实为了规避堆上数据重复(深拷贝)的同时又规避double free的一种设计方案,但给编码人员带来的麻烦就是,要明确知道s1是什么类型才能确定后续是否可以继续使用s1。
变量与数据交互的方式(二):克隆
确实 需要深度复制 String
中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone
的通用函数。
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
rust在设计上,从语言语义的角度规避了c++语言使用过程中,可能存在的堆内存使用问题,但也会给出另一种方案来解决堆内存使用的部分场景(比如用户期望深拷贝)。
如果一个类型拥有 Copy
trait,一个旧的变量在将其赋值给其他变量后仍然可用。换句话说类型的copy trait注解决定了是否适用这个所有全转移。
如下:
- 所有整数类型,比如
u32
。 - 布尔类型,
bool
,它的值是true
和false
。 - 所有浮点数类型,比如
f64
。 - 字符类型,
char
。 - 元组,当且仅当其包含的类型也都是
Copy
的时候。比如,(i32, i32)
是Copy
的,但(i32, String)
就不是。
基本数据类型变量存在于栈上,默认深拷贝,默认有 Copy
trait
Rust 不允许自身或其任何部分实现了 Drop
trait 的类型使用 Copy
trait,即只要成员或自身自定义了drop析构函数那么这个类型就无法使用copy注解
任何简单标量值的组合可以是 Copy
的,不需要分配内存或某种形式资源的类型是 Copy
的
整体而言,就是如果一个变量实现深拷贝需要额外操作,那么就不能使用copy注解。copy注解理论上只能用在变量可以完全bytecopy的情形。
但实际上,即使自身涉及到资源的申请和释放,只要不定义drop trait 其自身应该还是可以使用copy trait,但要自己保证资源的合理销毁(比如申请和释放资源通过其他接口,而非构造或者析构来申请或者释放),这是个人理解,待确认。
只在栈上的数据:拷贝
完全支持copy trait,不会发生所有权转移。
所有权与函数
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 移出作用域。不会有特殊操作
返回值与作用域
返回值也可以转移所有权
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("hello"); // 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 并移出给调用的函数
}
引用与借用
我们将获取引用作为函数参数称为 借用(borrowing)
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
&s1
语法让我们创建一个 指向 值 s1
的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域时其指向的值也不会被丢弃。
这里定义
fn calculate_length(s: &String) -> usize
及调用
let len = calculate_length(&s1);
都需要使用 &
个和c++有点差异,c++调用是时这样传表示s1的地址,即指针。
如果我们尝试修改借用的变量,需要定义可变引用
可变引用
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
Rust 可以在编译时就避免数据竞争
可变引用有一个很大的限制:在特定作用域中的特定数据只能有一个可变引用。
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
let r2 = &mut s;
也 不能在拥有不可变引用的同时拥有可变引用
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题
println!("{}, {}, and {}", r1, r2, r3);
一个引用的作用域从声明的地方开始一直持续到最后一次使用为止
所以如下代码是可以编译的:
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
println!("{} and {}", r1, r2);
// 此位置之后 r1 和 r2 不再使用
let r3 = &mut s; // 没问题
println!("{}", r3);
悬垂引用(Dangling References)
fn dangle() -> &String { // dangle 返回一个字符串的引用
let s = String::from("hello"); // s 是一个新字符串
&s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
// 危险!
所谓的悬垂引用和c++的悬垂指针意义差不多,都是引用或者指向了一个无效数据。
从C++上说即返回了局部变量的引用。
fn no_dangle() -> String {
let s = String::from("hello");
s
}
我们只能通过不返回引用的方式来返回局部变量,类似于c++的拷贝构造,不过rust中叫所有权转移。结果就是走到 }
时不再析构s(调用s.drop)。
引用的规则
让我们概括一下之前对引用的讨论:
- 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
- 引用必须总是有效的。