文章

Rust 所有权

Rust

Rust 所有权

Rust 所有权

所有权

所有权是 Rust 语言最为显著的特点,它让程序不用进行垃圾回收即可保证内存安全

所有权是什么?

有的编程语言拥有属于自己的垃圾回收机制,有的编程语言则需要开发者自己去实现垃圾回收,而 Rust 则是使用了第三种方式——拥有属于自己的所有权系统来管理内存

编译器在编译的时候,会根据一系列规则来进行检查,如果违反了某些规则,程序则不能通过编译。在运行时,所有权的一系列功能都不会影响程序,从而保证了原有程序的性能。也就是说,所有权不影响程序的性能

理解了所有权机制,就不用纠结栈和堆的问题,所有权机制的主要目的是为了管理堆上的数据

所有权机制

  1. Rust 中每个值都有自己的所有者(Owner),且每个值在任意时刻有且仅有一个所有者
  2. 当所有者或变量离开作用域时,这个值就会被抛弃

变量作用域

在 Rust 变量的作用域范围如下:

let name = "Hello World!";
 
// 作用域范围
{ // 这里是无效的,因为 name 还没有声明
	let name = "Hello World!"; // name 变量的作用域范围从这里开始
  ...
} // 作用域范围到这里结束 🔚

内存与分配

字符串字面值都是硬编码进二进制文件的,但是总有一些未知大小的字符串字面量,这些如何处理?

在 Rust 中,为了处理这些未知大小的字符串字面量,有一个 String 类型,它支持一个可变、可增长的文本值,并且将值分配给堆来存放内容。

这意味着:

  • 必须在运行时向内存分配器申请内存空间
  • 需要一个当 String 处理完时,将内存返还给内存分配器的方法

这看起来很像是生命周期的概念…

let name = String::from("Hello");
name.push_str(", World!");
println!("{name}");

依据所有权规则,name 变量在作用域结束时就被抛弃了,换句话说就是被自动释放了。

{
  let name = String::from("Hello"); // name 变量已声明,作用域开始
 
  .... // 使用 name 变量有效
} // 作用域结束,调用 drop 函数
// 使用 name 变量无效,在这里就会将 String 需要的内存返还给内存分配器

在变量离开作用域,即作用域结束时,Rust 提供了一个 drop 函数,在结尾的 } 处自动调用 drop 函数。

多个变量使用在堆上

移动

let x = 6;
let y = x;

将 6 绑定到变量 x 上,并且生成一个 x 的拷贝绑定到变量 y 上

let s1 = String::from("Hello");
let s2 = s1;

String 类型由三部分组成:一个指向存放字符串内容内存的指针(堆上存放内容的内存部分)、一个长度(表示当前使用了多少字节的内存)、一个容量(从内存分配器总共获取了多少字节的内存)。

s1 赋值给 s2 ,这意味着复制了 s1 的指针、长度、容量,但没有复制堆上的数据。

根据所有权的原则,当变量离开作用域后,Rust 会自动调用 drop 函数并清理变量的堆内存。此时 s1s2 指向了同一个同一内存,那么当 s1s2 离开作用域后,它们会尝试释放相同的内存。这是一个二次释放(double free)的错误。两次释放相同的内存会导致内存被污染,并导致可能存在潜在的安全漏洞

为了保证内存的安全,Rust 在 let s2 = s1 后,认为变量 s1 不再有效,因此 Rust 不需要在 s1 离开作用域后清理任何东西。

let s1 = String::from("Hello");
let s2 = s1;
 
println!("{s1}, World!");
99  |   let s1 = String::from("hello");
    |       -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
100 |   let s2 = s1;
    |            -- value moved here
101 |
102 |   println!("{}, world!", s1);
    |                          ^^ value borrowed here after move

Rust 禁止你去使用无效的引用。

拷贝了指针、长度、容量但没有拷贝堆上的数据,且 Rust 会同时使第一个变量无效,这被称为”移动”(move)

s1 被移动到了 s2 中,具体发生了什么,下图可以解释:

s1 变为无效的了,因为只有 s2 是有效的,当其离开作用域,它就释放自己的内存了。

Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何 自动 的复制都可以被认为是对运行时性能影响较小的。

克隆

如果确实需要深度复制堆上的数据,而不仅仅是栈上的数据,可以使用一个通用的函数 clone

let s1 = String::from("hello");
let s2 = s1.clone();
 
println!("s1 = {}, s2 = {}", s1, s2);
cargo run
   Compiling rust_tests v0.1.0 (/Users/liaoyangwu/Desktop/Coding/Rust学习/rust_tests)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/rust_tests`
s1 = hello, s2 = hello

拷贝

拷贝只适用于拷贝栈上的数据

let x = 6;
let y = x;
 
println!("x = {}, y = {}", x, y);
   Compiling rust_tests v0.1.0 (/Users/liaoyangwu/Desktop/Coding/Rust学习/rust_tests)
    Finished dev [unoptimized + debuginfo] target(s) in 0.07s
     Running `target/debug/rust_tests`
x = 6, y = 6

上面的代码看起来和之前说的有冲突呀,为什么没有调用 clone 函数,x 依然有效且没有被移动到了 y 呢?

原因是:像整型这样的在编译时已知大小的类型被整个存储在栈上。

Rust 有一个 Copy trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上。如果一个类型实现了 Copy trait ,那么一个旧的变量在赋值给其他变量后仍然可用

Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait,以下是一些常见的 Copy 的类型:

  • 所有整数类型
  • 布尔类型
  • 所有浮点数类型
  • 字符类型
  • 元组(当且仅当其包含的类型也都实现 Copye 的时候),如 (i32, i32) 实现了 Copy ,但 (i32, String) 就没有

所有权与函数

fn main() {
    let s = String::from("hello");  // s 进入作用域
 
    takes_ownership(s);             // s 的值移动到函数里 ...
                                    // ... 所以到这里不再有效
 
    let x = 5;                      // x 进入作用域
 
    makes_copy(x);                  // x 应该移动函数里,
                                    // 但 i32 是 Copy 的,
                                    // 所以在后面可继续使用 x
 
} // 这里,x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
  // 没有特殊之处
 
fn takes_ownership(some_string: String) { // some_string 进入作用域
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。
  // 占用的内存被释放
 
fn makes_copy(some_integer: i32) { // some_integer 进入作用域
    println!("{}", some_integer);
} // 这里,some_integer 移出作用域。没有特殊之处

返回值与作用域

返回值可以转移所有权

fn main() {
    let s1 = gives_ownership();         // gives_ownership 将返回值
                                        // 转移给 s1
 
    let s2 = String::from("hello");     // s2 进入作用域
 
    let s3 = takes_and_gives_back(s2);  // s2 被移动到
                                        // takes_and_gives_back 中,
                                        // 它也将返回值移给 s3
} // 这里,s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
  // 所以什么也不会发生。s1 离开作用域并被丢弃
 
fn gives_ownership() -> String {             // gives_ownership 会将
                                             // 返回值移动给
                                             // 调用它的函数
 
    let some_string = String::from("yours"); // some_string 进入作用域。
 
    some_string                              // 返回 some_string
                                             // 并移出给调用的函数
                                             //
}
 
// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
                                                      //
 
    a_string  // 返回 a_string 并移出给调用的函数
}

变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有

可以使用元组返回多个值:

fn main() {
    let s1 = String::from("hello");
 
  	// 解构
    let (s2, len) = calculate_length(s1);
 
    println!("The length of '{}' is {}.", s2, len);
}
 
fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() 返回字符串的长度
 
    (s, length)
}

以上代码可以通过引用来解决不用获取所有权就能够使用值的情况。