[Rust 入门] Rust 所有权

本教程环境:

系统: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 类型。

参考链接:

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

相关推荐
DongLi013 天前
rustlings 学习笔记 -- exercises/05_vecs
rust
番茄灭世神3 天前
Rust学习笔记第2篇
rust·编程语言
shimly1234564 天前
(done) 速通 rustlings(20) 错误处理1 --- 不涉及Traits
rust
shimly1234564 天前
(done) 速通 rustlings(19) Option
rust
@atweiwei4 天前
rust所有权机制详解
开发语言·数据结构·后端·rust·内存·所有权
shimly1234564 天前
(done) 速通 rustlings(24) 错误处理2 --- 涉及Traits
rust
shimly1234564 天前
(done) 速通 rustlings(23) 特性 Traits
rust
shimly1234564 天前
(done) 速通 rustlings(17) 哈希表
rust
shimly1234564 天前
(done) 速通 rustlings(15) 字符串
rust
shimly1234564 天前
(done) 速通 rustlings(22) 泛型
rust