学习 Rust 的第 4 篇笔记:所有权与借用。所有权是 Rust 最为与众不同的特性,也是难点之一。它的作用是解决内存安全问题,在其它语言中也存在类似的机制,比如:
- 垃圾回收机制(GC) ,在程序运行时不断寻找不再使用的内存,典型代表:Java、Go
- 通过函数调用的方式来申请和释放内存,典型代表:C++
所有权机制优势:只在编译时根据一系列规则进行检查,在程序运行期不会有任何性能上的损失。
栈(Stack)与堆(Heap)
对于 Rust,需要能够判断值是位于栈上还是堆上, 因为这会影响程序的行为和性能。
栈和堆的目的:为程序在运行时提供可供使用的内存空间
栈
- 按照顺序存储值并以相反顺序取出值,即:后进先出
- 增加数据叫做进栈,移出数据则叫做出栈
- 数据占用的内存空间:已知且固定大小
堆
-
堆是一种缺乏组织的数据结构,其中的数据大小未知且可能发生变化
-
在堆上分配(allocating)内存的过程:
- 在堆的某处找到一块足够大的空位
- 把该区域标记为已使用,返回一个表示该区域的指针
- 将该指针推入栈中,后续通过栈中的指针找到实际内存位置
对比
-
写入速度:
- 入栈(快):无需分配新的空间,只需要将新数据放入栈顶
- 在堆上分配内存(慢):过程更复杂
-
读取速度:
- 栈(快):可以直接存储在 CPU 高速缓存
- 堆(慢):只能存储在内存,且须要访问栈上的指针)
-
结论:处理器处理分配在栈上数据会比在堆上的数据更加高效
所有权与堆栈
调用函数:参数(堆上指针、局部变量)按顺序入栈,调用结束按相反顺序移除。
所有权:追踪堆上的数据的 分配 和 释放
所有权原则
规则:
- 每一个值都被一个变量所拥有,该变量被称为值的所有者
- 一个值只能拥有一个所有者
- 当所有者离开作用域范围时,这个值将被丢弃(drop)
字符串字面量与 String 类型
字符串字面量:let s ="hello",类型为 &str(字符串Slice)
不适用所有使用字符串的场景,因为:
- 字符串字面量被硬编码进程序里,是不可变的
- 字符串的值不一定能在编写代码时得知,比如用户输入
rust 提供了 String 类型:let s = String::from("hello");,
- 根据字面量创建 String 类型变量,
::是调用操作符 - 分配在堆上,存储在编译时大小未知的文本
let mut s = String::from("hello"); // 可变
s.push_str(", world!"); // push_str() 在字符串后追加字面值
println!("{}", s); // 将打印 `hello, world!`
转移所有权 move
// 1. 通过自动拷贝的方式来赋值
let x = 5; // 固定大小的简单值,存在栈中,无需在堆上分配内存。
let y = x; // 自动拷贝,直接复制内存
// 2. 字面量,不可变引用
let x: &str = "hello, world"; // 只引用了存储在二进制中的字符串,并没有持有所有权
let y = x; // 仅仅是对该引用进行了拷贝
println!("{},{}",x,y);
// 3. String 类型由3方面构成:
// 存储在栈中的堆指针、字符串长度(已使用大小)、字符串容量(堆内存分配的大小)
let s1 = String::from("hello");
let s2 = s1; // 所有权从 s1 转移给 s2,s1 立即失效
println!("{}, world!", s1); // 报错:use of moved value: `s1`
// 假如不 drop s1,离开作用域释放相同的内存,即造成二次释放(double free) 的错误
深拷贝 clone
Rust 永远也不会自动创建数据的 “深拷贝”,任何自动的复制都不是深拷贝
let s1 = String::from("hello");
let s2 = s1.clone(); // 使用 clone 方法深拷贝复制堆上数据
// 对于执行较为频繁的代码(热点路径),使用 clone 会极大的降低程序性能,小心使用
println!("s1 = {}, s2 = {}", s1, s2);
浅拷贝 copy
只发生在栈上。在编译时是已知大小,被存储在栈上的值拥有 copy 特征,即一个旧的变量在被赋值给其他变量后仍然可用。这样的类型包括:
- 所有整数类型,比如
u32 - 布尔类型,
bool,它的值是true和false - 所有浮点数类型,比如
f64 - 字符类型,
char - 元组(包含的类型必须能
Copy):(i32, i32)可以,但(i32, String)不行 - 不可变引用
&T,例如&str,但是注意: 可变引用&mut T是不可以 Copy的
函数传值与返回
传值
堆上数据传给函数,所有权从当前作用域转移到函数里
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值移动到函数里,所以到这里不再有效
let x = 5; // x 进入作用域
// x 应该移动函数里,但 i32 是 Copy 的,所以在后面可继续使用 x
makes_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 进入作用域
// s2 被移动到takes_and_gives_back 中,它也将返回值移给 s3
let s3 = takes_and_gives_back(s2);
// s3 移出作用域并被丢弃
// s2 也移出作用域,但已被移走,所以什么也不会发生
// s1 移出作用域并被丢弃
}
// gives_ownership 将返回值移动给调用它的函数
fn gives_ownership() -> String {
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 并移出给调用的函数
}
- 避免了内存的不安全性
- 新麻烦:总是把一个值传来传去来使用它,语言表达变得非常啰嗦
引用与借用
&T 引用:允许你使用值但不获取其所有权,创建引用的过程称为借用(borrowing)
*T 解引用:解出引用指向的值
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
assert_eq!(5, y); // error[E0277]: can't compare `{integer}` with `&{integer}`
不可变引用
变量默认不可变一样,引用指向的值默认也是不可变的
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
change(&s1);
}
fn calculate_length(s: &String) -> usize {
s.len() // 允许你使用值,但是不获取所有权
}
// cannot borrow `*some_string` as mutable, as it is behind a `&` reference
fn change(some_string: &String) {
some_string.push_str(", world");
}
可变引用
&mut T:创建可变引用
特点:
- 同一作用域,特定数据只能有一个可变引用,可以拥有多个不可变引用
- 可变引用与不可变引用不能同时存在,无法同时借用可变和不可变
- 编译器会找到某个引用在作用域结束前就不再使用的位置:Non-Lexical Lifetimes(NLL)
- 禁止悬垂引用(Dangling References),也叫做悬垂指针:值被释放后,指针依然存在
fn main() {
let mut s = String::from("hello");
change(&mut s);
let r1 = &mut s;
// 1. 同一作用域,特定数据只能有一个可变引用:避免数据竞争,竞态条件
// 多个的指针同时访问同一数据,其中一个指针被用来写入数据,没有同步数据访问的机制
let r2 = &mut s; // cannot borrow `s` as mutable more than once at a time
println!("{}, {}", r1, r2);
// 可以通过括号创建一个新的作用域,避免同时拥有
{
let r1 = &mut s;
}
let r2 = &mut s; // ok
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
// 2. 无法同时借用可变和不可变
// 避免可变引用修改了值,不可变引用没同步
fn exist() {
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
// cannot borrow `s` as mutable because it is also borrowed as immutable
let r3 = &mut s;
println!("{}, {}, and {}", r1, r2, r3);
}
// 3. nll,Rust 1.31以后
fn nll() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// 新编译器中,r1,r2作用域在这里结束
let r3 = &mut s;
println!("{}", r3);
} // 老编译器中,r1、r2、r3作用域在这里结束
// 新编译器中,r3作用域在这里结束
// 4. 垂悬指针
fn dangle() -> &String { // error[E0106]: missing lifetime specifier
let s = String::from("hello");
&s
} // s 离开作用域并被丢弃,其内存被释放,但是函数调用者依然持有指针
// help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
// help: consider using the `'static` lifetime