什么是所有权
所有权(ownership)是 Rust 用于管理内存的一组规则,通过在编译阶段检查程序内存使用的合法性,来避免运行时的内存安全问题。
所有权的优势:
- 运行时零开销,不会影响性能;
个人认为的缺点:
- 开发者的学习成本稍高,并随着程序的复杂度上升,需要付出更多的心智成本。
所有权原则
所有权的基本规则:
- Rust 中每一个值都被一个变量所拥有,该变量被称之为:值的所有者(owner);
- 一个值都是只能被一个变量所拥有,或者说,一个值只能拥有一个所有者;
- 当所有者(变量)离开作用域范围时,这个值将被丢弃(自动调用 drop)。
理解 Rust 中的堆栈,可以加深对所有权的理解。
变量绑定背后的数据交互
- 对于基本类型(存储在栈上), Rust 会自动拷贝;
- 否则不能自动拷贝(比如 String 类型)。
Rust 避免二次释放的策略: 对于复杂类型(由存储在栈中的堆指针、其他成员共同组成),比如 String,当发生所有权转移时,旧所有者将失效,因此不会在离开作用域时 drop。
Rust 移动(move): 区别于其他语言的浅拷贝(shallow copy),所有权发生移动后,旧所有者失效了。
Rust 克隆(clone): 首先,Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何自动的复制都不是深拷贝,可以被认为对运行时性能影响较小。
如果我们确实需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone 的方法。
let s1 = String::from("hello");let s2 = s1.clone();println!("s1 = {}, s2 = {}", s1, s2);
这段代码能够正常运行,说明 s2 确实完整的复制了 s1 的数据。
Rust 拷贝(浅拷贝): 浅拷贝只发生在栈上,因此性能很高,在日常编程中,浅拷贝无处不在。
let x = 5;let y = x;println!("x = {}, y = {}", x, y);
但这段代码似乎与我们刚刚学到的内容相矛盾:没有调用 clone,不过依然实现了类似深拷贝的效果 —— 没有报所有权的错误。
原因是像整型这样的基本类型在编译时是已知大小的,会被存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y 后使 x 无效(x、y 都仍然有效)。换句话说,这里没有深浅拷贝的区别,因此这里调用 clone 并不会与通常的浅拷贝有什么不同,我们可以不用管它(可以理解成在栈上做了深拷贝)。
Rust Copy : Rust 有一个叫做 Copy 的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy 特征,一个旧的变量在被赋值给其他变量后仍然可用,也就是赋值的过程即是拷贝的过程。
那么什么类型是可 Copy 的呢?可以查看给定类型的文档来确认,这里可以给出一个通用的规则:
任何基本类型的组合可以 Copy ,不需要分配内存或某种形式资源的类型是可以 Copy 的。
如下是一些可以 Copy 的类型:
- 所有整数类型,比如 u32;
- 布尔类型,bool,它的值是 true 和 false;
- 所有浮点数类型,比如 f64;
- 字符类型,char;
- 元组,当且仅当其包含的类型,也都是 Copy 的时候,比如 (i32, i32)
- 不可变引用 &T,注意:可变引用 &mut T 不可以 Copy。
函数传值与返回
将值传递给函数,一样会发生 移动 或者 复制,就跟 let 语句一样,下面的代码展示了所有权、作用域的规则:
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 移出作用域。不会有特殊操作
你可以尝试在 takes_ownership 之后,再使用 s,看看如何报错?例如添加一行 println!("在move进函数后继续使用s: ",s);。
同样的,函数返回值也有所有权,例如:
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 并移出给调用的函数}
所有权很强大,避免了内存的不安全性,但是也带来了一个新麻烦: 总是把一个值传来传去来使用它。 传入一个函数,很可能还要从该函数传出去,结果就是语言表达变得非常啰嗦,幸运的是,Rust 提供了新功能解决这个问题。