掘金 后端 ( ) • 2024-04-19 17:40

本教程环境:

系统:MacOS

Rust 版本:1.77.2

对于内存管理,都会希望编程语言具备两个特点:

  • 内存能在选定的时机及时释放,这样能控制内存的消耗;
  • 对象释放之后,绝不再使用指向它的指针;因为这种行为是未定义行为,会导致崩溃和安全漏洞。

但是两种情景很难兼顾,主流的编程语言都只能二选一。

  • "安全优先" 阵营会选择垃圾回收机制来管理内存,在所有执向对象的可达指针都消失了,自动释放内存。例如 Python、JavaScirpt、Ruby、Java 等。这种情况放弃了对释放对象时机的精准控制。
  • "控制优先" 阵营会让程序员自己释放内存。例如 C 和 C++ 语言。

Rust 的目标是既安全又高效。使用了独特的所有权机制。

所有权

在 Rust 中所有权的概念内置在语言本身,通过编译期检查强制执行。每一个值都有决定其生命周期的唯一拥有者。当拥有者被释放时,它拥有的值也会被释放。 所有权规则:

  • Rust 中每个值都有一个所有者
  • 值在任何时刻有且仅有一个所有者。
  • 当所有者离开作用域时,这个值将被丢弃。

在 Rust 中释放的行为称为 丢弃(drop)

变量作用域

作用域是一个变量在程序中能有效使用的范围。一个 {} 块就定义了一个作用域范围。

{
                        // s 在这里无效,尚未声明
    let s = "hello";    // 从此开始,s 有效

    // {} 范围内使用
} 			     // 超出作用域, s 被丢弃,不再有效

当变量离开作用域时候, Rust 会自动为我们调用一个 drop 函数,进行内存释放。

移动 - move

在 Rust 中,对大多数类型来说,为变量赋值、将其传给函数或从函数中返回等操作都不会复制值,而是移动值。源变量会将值的所有权转移给目标并变为未初始化状态,改由目标来控制该值的生命周期。 对于赋值操作来说。Python 需要引用计数,让赋值的开销很低。C++ 则选择让全部内存的所有权保持清晰,代价是在赋值时执行对象的深拷贝。 Rust 中如何处理呢?大多数类型会将值从源转移到目标,而源会变为未初始化状态。这样既开销低,同时所有权也是明确的。但是如果要同时访问这两个变量的话,需要进行深拷贝。 如果将一个值转移给已初始化的变量,那么 Rust 就会丢弃该变量先前的值。

let mut s = "hello".to_string();
s = "world".to_string(); // 丢弃了值 "hello"

比较下面的代码:

let mut s = "hello".to_string();
let t = s;
s = "world".to_string(); // 什么也没丢弃

因为 s 赋值给 t 后,s 变为未初始化状态,再赋值不会丢弃任何值。 发生移动的一些场景:

  • 初始化;
  • 赋值;
  • 从函数返回;
  • 构造出新值;例如,为结构体的某个字段使用 to_string返回值初始化。该结构体拥有这个字符串的所有权。
  • 将值传递给函数。

禁止在循环中进行变量的移动。

let x = vec![10, 20, 30];
while f() {
	g(x); // 报错:x 在第一次移动后变为未初始化状态
}

在控制流中,如果一个变量的值可能已经移走,并且从那以后没有赋予其新值,就可看作是未初始状态。

let x = vec![10, 20, 30];
if c {
  f(x); // 可能在这里移动 x
} else {
  g(x); // 也可能在这里移动 x
}
h(x); // 报错,x 在这里是未初始化状态

如果向量尝试通过赋值的方式通过索引进行移动,会报错。

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>`

此时,如果是需要访问这个元素,使用引用即可。如果确实需要移动元素,有以下方式:

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 进行了移动。

let v = vec!["hello".to_string(), " world".to_string(), "!".to_string()];
for mut s in v {
	s.push('!');
	println!("{}", s);
}
// hello!
//  world!
// !!

如何移动 Option 类型的值?

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 方法一样。

let first_name = composers[0].name.take();

Copy 类型:移动的例外情况

对于简单类型,例如整数、字符等,赋值的时候会直接进行拷贝操作,而不是移动。 这些类型被 Rust 指定为 Copy 类型。 标准的 Copy 类型包括所有机器整数类型、浮点数类型、char 类型、bool 类型等。Copy 类型组成的元组或固定大小的数组本身也是 Copy 类型。 默认下,Struct 类型和 enum 类型不是 Copy 类型。但是如果结构体的所有字段本身都是 Copy 类型的,可以通过 #[derive(Copy, Clone)] 来声明这个自定义的结构体为 Copy 类型。

参考链接:

🌟🌟 🙏🙏感谢您的阅读,如果对你有帮助,欢迎关注、点赞 🌟🌟