本教程环境:
系统:MacOS
Rust 版本:1.77.2
对于内存管理,都会希望编程语言具备两个特点:
- 内存能在选定的时机及时释放,这样能控制内存的消耗;
- 对象释放之后,绝不再使用指向它的指针;因为这种行为是未定义行为,会导致崩溃和安全漏洞。
但是两种情景很难兼顾,主流的编程语言都只能二选一。
- "安全优先" 阵营会选择垃圾回收机制来管理内存,在所有执向对象的可达指针都消失了,自动释放内存。例如 Python、JavaScirpt、Ruby、Java 等。这种情况放弃了对释放对象时机的精准控制。
- "控制优先" 阵营会让程序员自己释放内存。例如 C 和 C++ 语言。
Rust 的目标是既安全又高效。使用了独特的所有权机制。
所有权
在 Rust 中所有权的概念内置在语言本身,通过编译期检查强制执行。每一个值都有决定其生命周期的唯一拥有者。当拥有者被释放时,它拥有的值也会被释放。 所有权规则:
- Rust 中每个值都有一个所有者。
- 值在任何时刻有且仅有一个所有者。
- 当所有者离开作用域时,这个值将被丢弃。
在 Rust 中释放的行为称为 丢弃(drop)。
变量作用域
作用域 是一个变量在程序中能有效使用的范围。一个 {}
块就定义了一个作用域范围。
rust
{
// s 在这里无效,尚未声明
let s = "hello"; // 从此开始,s 有效
// {} 范围内使用
} // 超出作用域, s 被丢弃,不再有效
当变量离开作用域时候, Rust 会自动为我们调用一个 drop
函数,进行内存释放。
移动 - move
在 Rust 中,对大多数类型来说,为变量赋值、将其传给函数或从函数中返回 等操作都不会复制值,而是移动 值。源变量会将值的所有权转移给目标并变为未初始化状态,改由目标来控制该值的生命周期。 对于赋值操作来说。Python 需要引用计数,让赋值的开销很低。C++ 则选择让全部内存的所有权保持清晰,代价是在赋值时执行对象的深拷贝。 Rust 中如何处理呢?大多数类型会将值从源转移到目标,而源会变为未初始化状态。这样既开销低,同时所有权也是明确的。但是如果要同时访问这两个变量的话,需要进行深拷贝。 如果将一个值转移给已初始化的变量,那么 Rust 就会丢弃该变量先前的值。
rust
let mut s = "hello".to_string();
s = "world".to_string(); // 丢弃了值 "hello"
比较下面的代码:
rust
let mut s = "hello".to_string();
let t = s;
s = "world".to_string(); // 什么也没丢弃
因为 s 赋值给 t 后,s 变为未初始化状态,再赋值不会丢弃任何值。 发生移动的一些场景:
- 初始化;
- 赋值;
- 从函数返回;
- 构造出新值;例如,为结构体的某个字段使用
to_string
返回值初始化。该结构体拥有这个字符串的所有权。 - 将值传递给函数。
禁止在循环中进行变量的移动。
rust
let x = vec![10, 20, 30];
while f() {
g(x); // 报错:x 在第一次移动后变为未初始化状态
}
在控制流中,如果一个变量的值可能已经移走,并且从那以后没有赋予其新值,就可看作是未初始状态。
rust
let x = vec![10, 20, 30];
if c {
f(x); // 可能在这里移动 x
} else {
g(x); // 也可能在这里移动 x
}
h(x); // 报错,x 在这里是未初始化状态
如果向量尝试通过赋值的方式通过索引进行移动,会报错。
rust
let mut v = Vec::new();
for i in 101..106 {
v.push(i.to_string());
}
let third = v[2]; // 报错
let fifth = v[4]; // 报错
// err: cannot move out fo index of `Vec<String>`
此时,如果是需要访问这个元素,使用引用即可。如果确实需要移动元素,有以下方式:
rust
let mut v = Vec::new();
for i in 101..106 {
v.push(i.to_string());
}
// 从向量末尾弹出一个值
let fifth = v.pop().expect("vector empty!");
// 将向量中指定索引的值和最后一个值互换,并将前者移动出来
let second = v.swap_remove(1);
// 把要取出的值和另一个值互换
let third = std::mem::replace(&mut v[2], "substitute".to_string());
像 Vec 这样的集合通常会在循环中消耗元素。在 for 循环中,v 进行了移动。
rust
let v = vec!["hello".to_string(), " world".to_string(), "!".to_string()];
for mut s in v {
s.push('!');
println!("{}", s);
}
// hello!
// world!
// !!
如何移动 Option
类型的值?
rust
struct Person { name: Option<String>, birth: i32 }
let mut composers = Vec::new();
composers.push(Person { name: Some("Palestrina".to_string()), birth: 1525 });
let first_name = std::mem::replace(&mut composers[0].name, None);
assert_eq!(first_name, Some("Palestrina".to_string()));
assert_eq!(composers[0].name, None);
由于 Option
的使用很普遍,为该类型专门提供了一个 take
方法。和上面的 replace
方法一样。
rust
let first_name = composers[0].name.take();
Copy 类型:移动的例外情况
对于简单类型,例如整数、字符等,赋值的时候会直接进行拷贝操作,而不是移动。 这些类型被 Rust 指定为 Copy 类型。 标准的 Copy 类型包括所有机器整数类型、浮点数类型、char 类型、bool 类型等。Copy 类型组成的元组或固定大小的数组本身也是 Copy 类型。 默认下,Struct 类型和 enum 类型不是 Copy 类型。但是如果结构体的所有字段本身都是 Copy 类型的 ,可以通过 #[derive(Copy, Clone)]
来声明这个自定义的结构体为 Copy 类型。
参考链接:
🌟🌟 🙏🙏感谢您的阅读,如果对你有帮助,欢迎关注、点赞 🌟🌟