移动语义 和 复制语义
转移语义:在进行复制或者传参时候,值会从原先的变量绑定 转移到新的变量绑定中。这也称作 所有权的转移,转移后,之前的变量绑定不可用。
复制语义:在进行复制或传入参数的时候,默认进行值复制,而不是转移所有权。
默认情况下,变量绑定都是 移动语义(Move Semantic),例如:
struct Foo;
let x = Foo;
let y = x;
println!("{:?}", x); // error: use of moved value
但是,如果类型实现了 Copy trait,那么该类型就具备了 复制语义(Copy Semantic)。
思考:什么类型可以实现 Copy 呢? 有没有 类型不能实现 Copy 呢?
智能指针 Box<T> 封装了原生指针,是典型的转移语义类型。 Box<T> 无法实现 Copy trait,意味着它被标记为了引用语义,禁止按位复制。 // 为啥无法实现 Copy 呢? 无法为 其他 crate 的 type 实现 其他 crate 的 trait。
基本的原生类型都是值语义,具有值语义的原生类型,在其作为右值进行赋值操作时,编译器会对其进行按位复制。
所有权机制
Rust 中的每个值(分配的内存)都有一个所有者,所有者拥有的权限:
- 控制资源的释放(不仅是内存)。
- 出借所有权,包括 不可变的 和 可变的。
- 转移所有权。
对于实现了(标记) Copy trait 的类型,具备了复制语义,就不存在所有权转移的问题。
默认实现了 Copy trait 的类型有: 基本数字类型,char类型,bool类型。以及由这些类型组成的 数组、元组 和 Option 类型。
绑定、作用域 和 声明周期
可变与不可变
- 分离可变与不可变性,方便了在适当的场景使用,选用 不用可变引用 或可变引用,或转移所有权。
- 当函数对变量产生副作用的时候,得明确要求传递的参数得是可变引用,或有所有权。
绑定的时间属性 -- 生存周期
变量绑定具有 “时空” 双重属性。
- 空间属性 是指标识符与内存空间进行了绑定。
- 时间属性 是指绑定的时效性,也就是指变量绑定的生存周期。
一个绑定的生存周期,也被称作 生命周期,是和词法作用域相关的。
目前 Rust 的生命周期是基于 词法作用域的,编译器能自动识别函数内部这些局部变量绑定的生命周期。
对于进入新的作用域的变量,也么复制值,要么 转移(或出借)所有权。
作用域的创建时刻
-
每个 let 声明都会穿件一个默认的词法作用域,该作用域就是它的声明周期。
fn demo_01() { let a = "hello"; -------------------| let b = "rust"; --------------'b 'a let c = "world"; -----------'c 'b 'a let d = c; -------'d 'c 'b 'a } --------|---|--|---| -
花括号: 可以使用花括号在函数体内创建词法作用域。
fn demo_02() { let outer_val = 1; let outer_sp = "hello".to_string(); { let inner_val = 2; outer_val; outer_sp; } println!(" outer_val = {}", outer_val); println!(" outer_sp = {}", outer_sp); // value borrowed here after move } -
match 匹配会产生一个新的词法作用域
fn demo_03() { let a = Some("hello".to_string()); match a { // can use '&a', then can use 'a' latter Some(s) => println!("s = {}", s), // value partially moved here _ => println!("nothing"), } println!("a = {:?}", a); // value borrowed here after partial move } -
循环语句(for, loop, while)
fn demo_04() { let v = vec![1, 2, 3]; for i in v { // v` moved due to this implicit call to `.into_iter()`; // help: consider borrowing to avoid moving into the for loop: `&v` println!("i = {}", i); println!("v = {:?}", v); // value borrowed here after move } } -
if let 和 while let 块
fn demo_05() { let a = Option::Some("hello".to_string()); if let Some(b) = a { // value partially moved here // if let Some(b) = a { println!("b = {}", b); } println!("a = {:?}", a); // value borrowed here after partial move } fn demo_06() { let mut optional = Some(0); while let Some(x) = optional { if x > 9 { println!("Greater than 9, quit!"); optional = None; } else { println!("x = {}", x); // 这里 optional 是 Option<i32> 类型,因此是 复制语义。 println!("optional = {:?}", optional); optional = Some(x + 1); } } } -
函数:函数体本身是独立的词法作用域。
fn main() { let str = "hello".to_string(); demo_06(str); println!("str = {}", str); // value borrowed here after move } fn demo_06(s: String) { println!("s = {}", s); } -
闭包:闭包会创建新的作用域,对于环境变量来说有一下三种捕获方式:
-
对于复制语义类型,以不可变引用 (&T) 来捕获;
-
对于移动语义类型,执行移动语义 (move) 转移所有权来捕获;
-
对于可变绑定,如果在闭包中包含对其进行修改的操作,则以可变引用 (&mut) 来捕捉。
fn demo_07() { let s = "hello".to_string(); // s 是String, Move语义,所以按第二条捕获方式,会转移所有权 let join = |i: &str| s + i; let s2 = join(" rust"); println!("after join = {}", s2); println!("s = {}", s); // value borrowed here after move }
所有权借用
直观感受
假设需要写一个函数,用来修改传入的数组的第一个元素,实现方式有两种思路,代码如下:
fn main() {
let arr1 = [1, 2, 3];
modify_mut_array1(arr1);
println!("arr1 = {:?}", arr1); // arr1 = [1, 2, 3]
let arr1 = modify_mut_array1(arr1);
println!("arr1 = {:?}", arr1); // arr1 = [11, 2, 3]
let mut arr2 = [1, 2, 3];
modify_mut_array2(&mut arr2);
println!("arr2 = {:?}", arr2); // arr2 = [22, 2, 3]
}
fn modify_mut_array1(mut v: [i32; 3]) -> [i32; 3] {
v[0] = 11;
return v;
}
fn modify_mut_array2(v: &mut [i32; 3]) {
v[0] = 22;
}
-
modify_mut_array1: 传入可变绑定,然后返回修改后的数组。函数参数签名支持模式匹配,相当于使用let将v重新声明成了可变绑定。
-
modify_mut_array2: 传入可变借用,获得借用的可变所有权,直接使用修改即可,函数使用完毕后自动归还,外部继续使用。
引用与借用
引用(Reference) 是 Rust 提供的一种指针语义。
引用与指针的区别在于,指针保存的是其指向内存的地址,而引用可以看作某块内存的别名(Alias),使用引用需要满足编译器的各种安全检查规则。
引用分为 可变引用(&mut) 和 不可变引用(&), 也称作借用(Borrowing),通过 & 或 &mut 操作符来完成所有权租借。
借用所有权会让 所有者(owner),受到如下限制:
- 在不可变引用借用期间,所有者不能修改资源,并且也不能再进行可变借用。
- 在可变引用借用期间,所有者不能访问资源,并且也不能再出借所有权。
引用在离开作用域之时,就是其归还所有权之时。
使用 &mut 引用的例子如下:
fn main() {
let mut v = vec![4, 1, 5, 3, 2];
bubble_sort(&mut v);
println!("v = {:?}", v);
}
fn bubble_sort(v: &mut Vec<i32>) {
let mut n = v.len() - 1;
while n > 0 {
for j in 0..n {
if v[j] > v[j + 1] {
v.swap(j, j + 1);
}
}
n = n - 1;
}
}
借用规则
为了保证内存安全,借用必须遵循一下三个规则。
- 规则一: 借用的声明周期不能长于出借方(拥有所有权的对象)的生命周期。
- 规则二: 可变借用(引用)不能有别名(Alias),因为可变借用具有独占性。
- 规则三: 不可变借用(引用)不能再次出借为可变借用。
规则一是为了防止出现悬垂指针,规则二和三核心原则:共享不可变,可变不共享。
关于引用,还需注意一点:解引用操作会获得所有权。
在需要对移动语义类型(例如 &String)进行解引用操作时,这种情况,编译器不允许将借用s的所有权转移给append,违反规则三。代码如下所示:
fn join(s: &String) {
// move occurs because `*s` has type `String`, which does not implement the `Copy` trait
let append = *s;
}