Rust 所有权
零开销内存回收 的一种高效实现方式
Rust 是一种系统编程语言,其设计目的是确保内存安全并防止数据竞争,而不依赖垃圾回收器。这种内存安全性主要通过所有权系统来实现。
内存回收
硬件内存大小受限,数据泄漏,隐私安全
主流编程语言的内存回收机制对比
- 静态语言
- 在编译时对变量类型进行检查和确定的语言
- c,c++,rust
- 动态语言
- 在运行时进行类型检查和确定的语言
- javascript,python
写法对比
rust
fn main() {
let x: i32 = 5;
let y = 5;
println!("The value of x is: {},y is {}", x,y);
}
javascript
let a = 1; // double float
console.log(a);
差异对比
特性 | 静态语言 | 动态语言 |
---|---|---|
类型检查时间 | 编译时 | 运行时 |
类型安全 | 更安全,减少运行时类型错误 | 较灵活,但类型错误可能在运行时出现 |
性能 | 通常更高效,编译器优化 | 通常较低,运行时类型检查 |
灵活性 | 较低,需明确声明类型 | 较高,允许在运行时改变类型 |
代码简洁性 | 需要显式类型声明,代码相对冗长 | 通常更简洁,适合快速开发 |
开发工具支持 | 更强大的静态分析和重构工具 | 开发工具支持有限,但在快速开发上占优势 |
回收方式对比
C/C++
- 内存管理方式: 手动管理(Manual Management)
- 特点:
- 程序员通过
malloc
和free
(C)或new
和delete
(C++)手动分配和释放内存。 - 没有内置的垃圾回收机制。
- 程序员通过
- 优点:
- 高效且灵活,适用于对性能要求极高的系统级编程。
- 缺点:
- 容易出现内存泄漏、悬垂指针和缓冲区溢出等问题,需要非常小心的内存管理。
// 释放分配的内存
free(ptr); ptr = NULL; // 将指针设为 NULL,避免悬空指针
// 动态分配一个数组的内存
int n = 5;int *arr = (int *)malloc(n * sizeof(int));
JavaScript
- 内存管理方式: 垃圾回收(Garbage Collection)
- 特点:
- 浏览器和 Node.js 环境中均使用垃圾回收器(如 V8 引擎的垃圾回收器)。
- 采用标记-清除(Mark-and-Sweep)、标记-压缩(Mark-and-Compact)分代回收 等算法。
- 优点:
- 自动内存管理,适合快速开发和运行在多平台上的应用。
- 缺点:
- 垃圾回收机制在某些情况下可能导致性能问题,如 UI 线程停顿。
Rust
- 内存管理方式: 所有权系统(Ownership System)
- 特点:
- Rust 使用所有权系统进行内存管理,编译器在编译时通过静态分析来确保内存安全。
- 每个值都有一个所有者,在任何时候只能有一个有效的所有者。
- 通过借用(引用)机制来共享数据,同时保证数据竞争和悬垂指针的安全。
- 优点:
- 在编译时保证内存安全,没有运行时开销。
- 避免了数据竞争和悬垂指针。
- 缺点:
- 需要程序员理解和遵循所有权和借用规则,学习曲线较陡。
stack & Heap
+--------------------+ +--------------------+
| | | |
| 高地址 | | 高地址 |
| (High Address) | | (High Address) |
| | | |
| 堆 (Heap) | <----| 自由增长 |
| | | (Grows Freely) |
| | | |
| --- ||--------------------|
| | | |
| | | |
| 未使用 (Unused) | | 栈 (Stack) |
| | | |
| | | |
| --- ||--------------------|
| | | |
| 低地址 | | 低地址 |
| (Low Address) | | (Low Address) |
+--------------------+ +--------------------+
为什么要有所有权
double free , memo safety
drop
释放内存
let x = 5;
let y = x;
let s1 = String::from("hello");
let s2 = s1;
所有权规则
Rust 是一种系统编程语言,其设计目的是确保内存安全并防止数据竞争,而不依赖垃圾回收器。这种内存安全性主要通过所有权系统来实现.
1. 所有权的基本规则
Rust 的所有权系统有三个基本规则:
- 每一个值都有一个所有者(owner)。
- 在任一时刻,值只能有一个所有者。
- 当所有者离开作用域(scope),值会被丢弃(drop)。
变量的作用域
变量在声明的地方引入作用域,并在离开作用域时释放资源:
{
let s = "hello"; // s 是有效的
// 使用 s
} // 作用域结束,s 被丢弃
fn a() {
let s = "hello"; // s 是有效的
}
// s
3. 所有权的转移:移动(Move)
当变量赋值给另一个变量时,所有权会发生转移:
let a = 1;
let b =a;
let s1 = String::from("hello");
let s2 = s1;
// 现在 s1 无效,所有权转移到了 s2
在这段代码中,s1
将其所有权转移给了 s2
,因此在 s1
被使用之后会导致编译错误。
课后习题
用2种方式实现,确保 s1
s2
都能正常打印出来
// TODO:
fn take_ownership(s: String) -> String {
s
}
let s1 = String::from("Hello");
let s2 = take_ownership(s1);
// 如下代码不能修改
println!("{}", s1);
println!("{}", s2);
引用 References
- 不可变引用(Immutable Reference):通过不可变引用,可以读取数据,但不能修改数据。一个变量可以有多个不可变引用,但不能与可变引用共存。
- 可变引用(Mutable Reference):通过可变引用,可以读取和修改数据。一个变量在某一时刻只能有一个可变引用,且不能与不可变引用共存。
引用的规则
- 同一时间内,一个变量只能有一个可变引用或多个不可变引用。
- 引用必须总是有效。
引用规则 vs 所有权规则
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{r1} and {r2}");
let r3 = &mut s;
println!("{r3}");
move & borrowing & referencing
- move 堆数据所有权
- borrowing 函数
- referencing 变量
切片(Slices)
- 字符串切片
- 数组切片
字符串切片是对字符串部分内容的引用:
let s = String::from("hello world");
let hello = &s[0..5]; // 引用 "hello"
let world = &s[6..11]; // 引用 "world"
数组切片
数组切片与字符串切片类似:
let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..3]; // 引用 [2, 3]
生命周期(Lifetimes)
dangling referencing
生命周期是Rust用来保证引用有效性的机制。生命周期注解允许编译器推断引用的有效范围,确保在引用仍然有效时使用它们。
#[test]
fn test_dangling_pointer() {
let a = get_a();
fn get_a() -> &String {
&"a".to_string()
}
}
课后习题
通过编译器错误提示,修复并运行代码
#[test]
fn test_lifetime() {
let large = longest("a", "ab");
println!("large one is {large}");
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
}