Rust_Ownership
Rust在编译时避免未定义行为而不是在运行时
使用
Box
来在堆中定义变量:1
2let a = Box::new([0; 1_000_000]);
let b = a; // a的所有权被移动到bfn dereferencing() { let mut x: Box<i32> = Box::new(1); println!("x = {}", x); let a: i32 = *x; *x += 1; let r1: &Box<i32> = &x; // r1 points to x on the stack /** * println!("{}", variable):用于用户友好的显示。它会通过 Display trait 自动解引用指针和引用,直到找到一个具体的值来显示。 * println!("{:p}", variable):用于调试和底层观察。它会打印出引用或指针变量本身存储的那个内存地址。 */ println!("r1 = {:p}", r1); /** * 下面是错误的, 因为rust为了内存安全规定: 一个值在同一时间只能有一个owner; */ // let b: Box<i32> = *r1; let b: i32 = **r1; println!("b = {}", b); let r2: &i32 = &*x; // &*x 确实得到了 *x 所在位置的“地址”,但 Rust 把它包装成了一个更安全、更受限制的类型,叫做“引用 (&)” println!("r2 = {}", r2); let c = *r2; println!("c = {}", c); }
一些误区:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
```mermaid
graph LR
subgraph "内存区域"
subgraph "栈 (Stack Memory)"
direction TB
addr_x["@ 0x7ffc_A0"] -- "变量 x" --> val_x["<b>值: 0x55ef_B0</b><br/>(类型: Box<i32>)"]
addr_a["@ 0x7ffc_A8"] -- "变量 a" --> val_a["值: 1<br/>(类型: i32)"]
addr_r1["@ 0x7ffc_AC"] -- "变量 r1" --> val_r1["<b>值: 0x7ffc_A0</b><br/>(类型: &Box<i32>)"]
addr_b["@ 0x7ffc_B4"] -- "变量 b" --> val_b["值: 2<br/>(类型: i32)"]
addr_r2["@ 0x7ffc_B8"] -- "变量 r2" --> val_r2["<b>值: 0x55ef_B0</b><br/>(类型: &i32)"]
addr_c["@ 0x7ffc_C0"] -- "变量 c" --> val_c["值: 2<br/>(类型: i32)"]
end
subgraph "堆 (Heap Memory)"
addr_heap["@ 0x55ef_B0"] -- "Box<T>指向的数据" --> val_heap["值: 2<br/>(类型: i32)"]
end
end
%% 箭头表示“指向”关系
val_x -- "指向堆数据" --> addr_heap
val_r1 -- "引用栈上的 x" --> addr_x
val_r2 -- "引用堆上的数据" --> addr_heap
%% 样式
style val_x fill:#f9f,stroke:#333
style val_r1 fill:#ccf,stroke:#333
style val_r2 fill:#9cf,stroke:#333
style addr_heap fill:#f96,stroke:#333,stroke-width:2px这时候`***y` 才指向值`0`, `*y`是`x`的引用(`&x`)类型是`&Box<i32>`, `**y`指向`x`本身(`x: Box<i32>`), `***y`才指向`*x`(即x指向的堆的值`0`).1
2let x = Box::new(0);
let y = Box:new(&x);Rust Avoids Simultaneous Aliasing and Mutation
Pointer Safety Principle: data should never be aliased and mutated at the same time.
数据不允许同时别名和修改
以
Vec
为例:1
2let mut v: Vec<i32> = vec![1, 2, 3];
v.push(4);Vec
的结构是这样的:,
Vec
是一个在栈上的结构体,它包含三个部分:一个指向堆内存的指针 (pointer)、长度 (length) 和容量 (capacity). 其中len
代表Vec
中实际有多少个元素,capacity
代表v
的容量是多少(可以放多少元素), 如果v.push
之后超过了capacity
,buf
中的指针就会重新开辟一块内存转移数据并且释放掉原先的内存.那么如果进行下面的操作
1
2
3
4let mut v: Vec<i32> = vec![1, 2, 3]; // 这里使用vec!宏来初始化Vec容器, 那么默认len = capacity = 3
let n: &i32 = &v[2]; // n 是v[2]的不可变引用immutable reference
v.push(4); // 这里因为capacity不够, 所以v会重新开辟内存转移数据, 并且释放掉原先的内存
println!("{n}"); // 这里就会报错, 因为n指向的地址已经被释放掉了, 所以这里会报错, 但是如果没有这一句, 编译器则不会报错: 虽然n现在还是指向已经释放的地址,但是编译器会自动判断n在释放地址之后没有再次使用,所以这里编译器不会报错*References Change Permissions on Places
Permission:
- Read (R): data can be copied to another location.
- Write (W): data can be mutated.
- Own (O): data can be moved or dropped.
Place: 任何可以放在赋值符号左侧的都可以是place, 比如: Variables, Dereferences of places, Array accesses of places, Field of places like tuples or
a.field
for structs, any combination of the aboveplace默认有
R(ead)
,O(wn)
的权限, 如果赋值的时候使用mut
符号, 那么还会增加W权限1
2let v: Vec<i32> = vec![1, 2, 3]; // v 只有R和O权限
let mut v1: Vec<i32> = vec![1, 2, 3]; // v1 有W,R,O权限**首先要明确, 如果place离开作用域, 权限就会被收回, eg: **
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15fn main() {
let v: Vec<i32> = vec![1, 2, 3]; // 此时v有R和O权限
{
let v1: Vec<i32> = vec![1, 2, 3]; // 此时v1有R和O的权限
} // 这里v1的作用域结束, v1失去所有权限
println!("{:?}", v);
change(v); // 因为Vec没有实现Copy Trait所以v是被move到了这个函数中, v的权限也随之移动到了函数中, 当函数结束时, 意味着v的作用域结束, v的权限此时被收回;
println!("{:?}", v); // 这里会报错, v已经失去了所有权限
} // main函数是v的作用域, 所以只有在main函数结束的时候, v才会失去所有权限
fn change(v: Vec<i32>) {
println!("{:?}", v);
}一个解决方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// 第一个解决方法
let mut v: Vec<i32> = vec![1, 2, 3];
let v_again = change(v);
fn change(v: Vec<i32>) -> Vec<i32> {
println!("{:?}", v);
v
}
println!("{:?}", v_again);
// 第二个解决方法
let mut v: Vec<i32> = vec![1, 2, 3];
change(v.clone()); // 使用clone方法, 但是clone方法会在堆中新开辟一块内存, 所以会有性能问题
println!("{:?}", v);
fn change(v: Vec<i32>) {
println!("{:?}", v);
}
// 第三个方法 immutable references
let mut v: Vec<i32> = vec![1, 2, 3];
change(&v); // 使用引用的方式, 这里不会在heap中创建副本
println!("{:?}", v);
fn change(v: &Vec<i32>) {
println!("{:?}", v);
}**immutable references(shared references): ** 可以在不移动所有权的情况下提供临时的访问, 但是不能修改数据. 同一时间可以有很多Immutable references
**mutable references(unique references): ** 可以对数据进行修改, 但是同一时间只能有一个mutable reference
1
2
3
4
5
6
7
8
9
10let mut v: Vec<i32> = vec![1, 2, 3];
let num: &mut i32 = &mut v[2];
/**
* 此时v失去所有权限
* num只是一个存储v[2]地址的一个指针(引用), 所以num对于它自己本身有R和O的权限(没有使用mut), 注意: v[2]是v内部buf中的ptr地址, 但是v结构的地址不会变
* *num 此时才是存储的v[2]的值, 此时*num有R和W权限(因为是mutable reference), 但是引用就是引用, 所以没有O权限
*/
*num += 1;
v.push(4);O
(Own) 权限永远属于数据的所有者(在这里是v
)。引用 (&
或&mut
) 只是临时“借用”了R
或R
+W
权限。*num
作为一个“位置 (place)”,它允许被读取和写入,但对这块数据的“生杀大权”(移动或销毁)仍然在v
手里。some questions:
Q&A:
1
2
3
4
5
6
7fn main() {
let mut s = String::from("Hello");
let t = &mut s;
/* here */ // s在这里没有任何权限, 因为t是mutable reference, 所以当t生命周期没有结束的时候, s失去了所有权限
t.push_str(" world");
println!("{}", s);
}*Data Must Outlive All Of Its References
直接来看例子:
1
2
3
4
5
6fn main() {
let s = String::from("Hello world");
let s_ref = &s;
drop(s); // 这里s还有引用s_ref, 所以如果这里drop掉, s_ref就会指向无效地址, 如果后续s_ref还有使用(生命周期没有结束), 编译器就会自动检测并报错
println!("{}", s_ref); // s_ref生命周期没有结束, 但是现在s_ref指向无效地址了, 所以编译器会报错
}因为编译器无法看到所有函数的周期, 他只能看到函数的signature(输入和输出), 所以如果引用从函数中返回的时候是安全的, 就会获得
F
权限F(low)
权限: 视为 “出口许可证”。 只有当编译器能够证明该引用从其当前上下文中“流出”(例如从函数返回)是安全的时,引用才会获得F
权限。1
2
3
4fn first(strings: &Vec<String>) -> &String {
let s_ref = &strings[0];
s_ref
}不安全的行为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/**
* 这里的函数是会报错的,因为函数不知道返回的String生命周期和哪个输入一致, 具体在main函数
*/
fn first_or<'a, 'b, 'c>(strings: &'a Vec<String>, default: &'b String) -> &'c String {
if strings.len() > 0 {
&strings[0]
} else {
default
}
}
fn main() {
let strings = vec![]; // 这里strings是一个空的Vec
let default = String::from("default");
let s = first_or(&strings, &default); // 根据first_or函数的定义: 这里会返回default, 所以s的生命周期也和defalut保持一致,但是这里有个问题: 编译器事先并不知道应该返回的生命周期该和strings保持一致还是和default保持一致, 如果和strings保持一致, 那么最后的println!宏会报一个清晰的错误:那么s的生命周期已经结束了, 因为string的生命周期在这一行结束就结束了, 如果和default保持一致, 那么经过下一句, s就会变成悬垂指针也会报错
drop(default);
println!("{}", s);
}另一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15fn main() {
let default = String::from("I live for a long time"); // `default` 的生命周期很长
let final_ref;
{ // 进入一个新的、更短的生命周期范围
let strings = vec![String::from("I live for a short time")]; // `strings` 的生命周期很短
// 调用这份模糊的合同
final_ref = first_or(&strings, &default);
} // `strings` 在这里被销毁了!它的内存被回收了!
// 灾难发生点:
println!("{}", final_ref); // `final_ref` 现在指向的是谁?
}修改后:
1
2
3
4
5
6
7fn first_or2<'a>(strings: &'a Vec<String>, default: &'a String) -> &'a String {
if(strings.len() > 0) {
&strings[0]
} else {
default
}
}- 我们定义了一个生命周期标签,名叫
'a
。 - 输入
strings
必须至少活得和'a'
一样长。 - 输入
default
也必须至少活得和'a'
一样长。 - 最重要的承诺:函数返回的引用,其有效期最多和
'a'
一样长。
这里的
'a
实际上代表了strings
和default
中较短的那个生命周期。编译器看到调用
first_or(&strings, &default)
。它比较了
strings
(生命周期短)和default
(生命周期长)的有效期。它确定了那个更短的有效期,并将其作为这次调用的
'a
。函数返回的
final_ref
因此也获得了这个较短的生命周期'a
。所以,
final_ref
的有效范围被严格限定在了那个内部{}
之中。当代码试图在
{}
外部使用println!("{}", final_ref)
时,审查员会立刻发现:final_ref
的生命周期已经结束了,它已经失效了!审查员报告一个新的、明确的错误:“你正在使用一个已经过期的引用。”
重点: ** 当一个函数返回的引用可能来自多个不同的输入时,编译器会因为无法判断这个引用的“保质期”而拒绝编译。生命周期
'a
就是你给编译器提供的一个明确的“保质期”标签**,你向它承诺,返回的引用的有效期不会超过所有相关输入中最先失效的那个。这样,编译器就能确保安全了。Q1:
1
2
3
4
5
6
7
8fn incr(n: &mut i32) {
*n += 1;
}
fn main() {
let mut n = 1;
incr(&n);
println!("{n}");
}上面这个程序不会通过编译, 因为
incr(&n)
传递的是immutable reference, 应该修改为incr(&mut n)
Q2:
1
2
3
4
5
6
7fn main() {
let mut s = String::from("hello");
let s2 = &s;
let s3 = &mut s;
s3.push_str(" world");
println!("{s2}");
}这个程序也无法通过编译, 因为s2是immutable reference,所以在s2生命周期还没有结束的时候, 对s3操作是不合法的:
借用规则:“你不能在存在一个活跃的不可变借用的同时,再创建一个可变借用。”Q3:
Consider this Rust function that pushes a number onto the end of a vector, and then removes and returns the number from the front of the vector:
1
2
3
4fn give_and_take(v: &Vec<i32>, n: i32) -> i32 {
v.push(n);
v.remove(0)
}Normally, if you try to compile this function, the compiler returns the following error:
1
error[E0596]: cannot borrow `*v` as mutable, as it is behind a `&` reference --> test.rs:2:5 |1 | fn give_and_take(v: &Vec<i32>, n: i32) -> i32 { | --------- help: consider changing this to be a mutable reference: `&mut Vec<i32>`2 | v.push(n); | ^^^^^^^^^ `v` is a `&` reference, so the data it refers to cannot be borrowed as mutable
Assume that the compiler did NOT reject this function. Select each (if any) of the following programs that could possibly cause undefined behavior if executed. If none of these programs could cause undefined behavior, then check “None of these programs” .
**A: **这里主要的错误是
Vec
push的时候可能超过capacity, 导致v内部值的地址改变, 最终导致悬垂指针1
2
3
4
5
6
7
8
9
10
11
12
13
14let v = vec![1, 2, 3];
let n = &v[0];
give_and_take(&v, 4);
println!("{}", n); // 这里就会导致错误, 此时n为悬垂指针
let v = vec![1, 2, 3];
let n = &v[0];
let k = give_and_take(&v, 4); // 这里结束之后, n为悬垂指针了, 但是n在后续并没有使用(生命周期已经结束), 所以编译器不会报错
println!("{}", k);
let v = vec![1, 2, 3];
let v2 = &v;
give_and_take(&v, 4); // 这里改变的是v内部buf中ptr的值, 而不是v结构体的地址,所以当内部地址改变的时候v2内部也会发生改变, 不会报错
println!("{}", v2[0]);- 我们定义了一个生命周期标签,名叫
Summary
References provide the ability to read and write data without consuming ownership of it. References are created with borrows (&
and &mut
) and used with dereferences (*
), often implicitly.
However, references can be easily misused. Rust’s borrow checker enforces a system of permissions that ensures references are used safely:
- All variables can read, own, and (optionally) write their data.
- Creating a reference will transfer permissions from the borrowed place to the reference.
- Permissions are returned once the reference’s lifetime has ended.
- Data must outlive all references that point to it.