认识所有权
所有权是 rust
独特的功能,它让 rust
无需垃圾回收即可保证内存安全。
什么是所有权
Rust
核心功能之一是所有权。所有运行的程序都必须管理其使用的计算内存的方式。一些语言具有内存回收机制,在运行时不断地寻址不再使用的内存。在另一些语言中,程序员必须亲自分配和释放内存。Rust
则使用第三中方式:通过所有权系统管理内存,在编译时会根据一系列规则检查进行检查。在运行时,所有权的任何功能不会减慢程序。
所有权规则
-
Rust
中的每一个值都有一个被称为其 所有者的变量 - 值有且只有一个所有者
- 当所有者离开作用域,这个值将被丢弃
变量作用域
{
// s 在这里无效,尚未声明
let s = "abc";
// 使用 s
} // 此作用域已结束 s 不在有效
内存与分配
就字符串字面值来说,编译时就知道其内容,所以文本被直接硬编码到最终可执行文件。这使得字,值快速且高效。这是因为字面值的不可变性。不幸的是,我们不能为了每一个在编译时大小未知的文本而将一块内存放入二进制文件中,并且它的大小可能随着程序运行发生变化。
对于 String
类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。
这意味着:
- 必须在运行时向操作系统请求内存。
- 需要一个当我们处理完
String
时将内存返回给操作系统的方法。
第一部分:当调用String::from
时,它实现了请求所需内存。
第二部分:内存在拥有它的变量离开作用域时就被自动释放。
{
let s = String::from("abc");
// 使用 s
} // 此作用域结束 s 不再有效
当变量离开作用域时,Rust
给我们调用了一个特殊的 drop
函数。
变量与数据交互的方式(一):移动
let x = 5;
let y = x;
这里做了什么:将 5
绑定到 x
, x
拷贝到y
。现在 x
和y
都等于 5
。因为是已知的固定大小的值,所以两个 5
被放入到了栈中。
现在看看String
的版本:
let s1 = String::from("abc");
let s2 = s1;
看起来和上面的代码非常相识,现在假设和他们的运行方式相识:s1
拷贝到 s2
。不过事实上完全不是这样。
String::from
在堆内存申请了空间,s1
指向了申请的内存。let s2 = s1
只是 s2
拷贝了s1
指向的内存地址,长度和容量,并没有复制堆上的数据。如果Rust
也复制了堆上的数据,那么会对运行时的性能造成非常大的影响。
之前提到过变量离开作用域后会自动调用drop
函数并清理堆内存。这里s1
和s2
都指向同一堆内存地址。当s1
和s2
离开作用域时,他们会释放相同的内存,这可能是一个二次释放的错误,两次释放相同内存会导致内存污染,它肯能会导致潜在的安全漏洞。
为了确保内存安全,这种场景下Rust
的处理有另一个细节值得注意。与其拷贝分配的内存,Rust
则认为s1
不再有效,在s1
离开作用域后不需要清理任何东西,在s2
创建只后s1
也无法使用了。尝试编译会得到一个错误:
error[E0382]: borrow of moved value: `s1`
--> .\rust所有权.rs:4:20
|
2 | let s1 = String::from("abc");
| -- move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 | println!("{}", s1);
| ^^ value borrowed here after move
error: aborting due to previous error
如果你在其他语言听说过浅拷贝和深拷贝,这看起来像浅拷贝。不过因为Rust
使第一个变量无效了,这操作被成为移动。
变量与数据交互的方式(二):克隆
如果我们确实需要深度复制String
中堆上的数据。而不仅仅是栈上的数据,可以用一个叫做clone
的通用函数。
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
这段代码能正常运行,这里堆上的数据复制了
只在栈上的数据:拷贝
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
这段代码似乎和我们刚刚学到的矛:没有使用 clone
,不过依然有效且没有被移动到 y
中。
原因是像整形这样的在编译时已知大小的类型被整个储存在栈上,所以拷贝的值是快速的。这也意味着没有理由在创建 y
后使 x
无效。换句话说这里的深拷贝和浅拷贝没有什么不同。
Rust
有一个叫做 Copy
trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型。
一些Copy
的类型:
- 所以的整数类型,比如
u32
- 布尔类型,
bool
,它的值是true
和false
- 所有的浮点类型,比如
f64
- 字符类型,
char
。 - 元组,当且仅当其包含的类型都是
Copy
的时候。比如(i32, i32)
,但(i32, String)
就不是。
所有权与函数
将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或复制,就像复制语句一样。
fn main() {
let s = String::from("abc");
f1(s); // s 移动到了 f1
// 这里 s 不再有效
let x = 5;
f2(x); // 这里 copy 了x,x 还是可以使用
} // 这里 x 先移出了作用域,然后是 s。因为 s 的值已被移走,这里不会有特殊操作
fn f1(s: String) {
println!("{}", s);
} // 这里 s 移出了作用域,并调用 drop 方法。占用的内存被释放
fn f2(x: i32) {
println!("{}", x);
} // 这里 x 移出了作用域,不会有特殊操作
返回值与作用域
fn main() {
let s1 = f1(); // f1() 将返回值 移给 s1
let s2 = String::from("hello"); // s2 进入作用域
let s3 = f2(s2); // s2 被移动到 f2 中,它也将返回值移给 s3
} // 这里 s3 移出作用域,调用 drop,s2 移出作用域,但已被移走不会做任何操作。s1 移出作用域调用 drop
fn f1() -> String {// 将返回值移动给调用方
String::from("abc")
}
fn f2(s: String) -> String {// s 进入作用域
s // 返回 s 并移出作用域给调用方
}
在一个函数中都获取作用域,并接着返回所有权有些啰嗦。如果函数使用一个值但不获取所有权该怎么办呢?Rust
有一个功能叫做引用