rust所有权(Ownership)规则详解

mashuo 2023-09-09 12:19:05
Categories: Tags:

1. 什么是所有权

所有权是rust程序用于管理内存的一系列规则。众所周知,所有编程语言都需要在程序运行时对电脑的内存进行管理,不同编程语言对于内存的管理方式也不同,目前主要有三种主流的内存管理方式:

(1)手动管理内存
即程序编写者需要手动开辟、释放内存来实现对内存的使用和管理,典型的代表有:c,c++(c++在c17以后具备三种智能指针,基于对象的模式实现了内存的自动管理,但这与rust的所有权规则还是有所不同的,rust的所有权规则基于编译器检查实现,在安全性上更胜一筹)。这种方式让程序的编写具备相当高的自由度,但也会产生较大的内存安全隐患

(2)垃圾回收机制(Garbage Collection,GC)
垃圾回收(Garbage Collection,GC)是一种自动化的内存管理技术,用于在编程语言中识别和释放不再需要的内存对象,以防止内存泄漏和提高程序的性能。不同的编程语言和垃圾回收器可以采用不同的具体实现和策略,但这些基本的垃圾回收概念通常都会涉及标记不可达对象并释放它们的内存。代表有java,c#,python,javascript等。
GC机制一定程度上保证了内存的安全性,但相应的也付出了性能代价。

(3)编译器检查与手动管理结合
这种就是rust实现内存管理的方式:手动管理意味着你仍然可以手动开辟,释放内存,这给予了编程者较高的自由;但程序中对内存的操作都会在编译阶段被检查,以确保不会出现常见的内存错误。可以看出,这种机制的核心就在于编译器的检查机制,这也就是本文要讲解的内容:所有权规则(Owenership)。

所有权的规则十分简单,由以下三条规则组成(这里给出中英双语),这里所说的“所有者”,也即对“值”具备所有权的变量

  • 每个值都有一个所有者。当值被创建时,它会自动分配给其所有者。(Each value in Rust has an owner)
  • 值只能有一个所有者。如果尝试将值分配给另一个所有者,则原始所有者将无法再次访问该值。(There can only be one owner at a time)
  • 当所有者超出其作用域时,该值将被自动释放(When the owner goes out of scope, the value will be dropped)

所有权规则和生命周期规则可以说是rust的两大核心,属于牵一发而动全身的内容。要将所有权规则讲清楚,就必然会涉及生命周期、引用和借用、堆栈内存空间、深浅拷贝等内容,这些也往往是每个编程语言的核心内容。在这篇博客中,我会逐步讲解这些概念以及在rust中的实现,最终将所有权规则讲清楚。


2. 简单了解所有权


2.1 一个简单的例子

运行以下代码:

use std::io;

fn main() {
    let i: u8 = 1;
    let s = String::from("hello world");

    let i1 = i;
    let s1 = s;
    println!("i = {} , i1 = {}", i, i1);
    println!("s = {} , s1 = {}", s, s1);
}

编译时会提示以下错误

error[E0382]: borrow of moved value: `s`
  --> src/main.rs:10:34
   |
5  |     let s = String::from("hello world");
   |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
...
8  |     let s1 = s;
   |              - value moved here
9  |     println!("i = {} , i1 = {}", i, i1);
10 |     println!("s = {} , s1 = {}", s, s1);
   |                                  ^ value borrowed here after move
   |
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
   |
8  |     let s1 = s.clone();
   |               ++++++++

根据报错信息,可以看出:

  1. 在第五行,我们创建了一个String对象,这里提示s是一个“String”类型,没有扩展实现copy特性,这里简单看做将String字符串的值的所有权归s所有,这里实际上涉及到所有权规则1和2。

  2. s的值在赋给s1后,发生了move(后称为所有权转移),也就是说String::from(“hello world”)这个值的所有权原来归属s,执行了第8行的代码后,所有权转移到了s1,这里属于所有权规则2。

  3. 在第10行提示”value borrowed after move”,也即在值的所有权被转移后仍然借用,报错。也就是说,原来s中存储的值的所有权已经被转移给了s1,无法再通过s来获取值的借用,这也诠释了所有权规则2。


2.2 规律

通过上例可以总结如下规律:

  • 一个变量在赋值给其他变量时,其所指向的值的所有权也会转移给其他变量,从而导致该变量的生命周期提前结束。

但是在上例中,明显有一处代码并不符合总结的规律,也就是变量i,其值赋给了变量i1,那按理说值”1“的所有权也移交给了变量i1,从而变量i生命周期结束,但从程序编译的结果看来并非如此。这里可能有人猜测:是由于基础类型变量u8存储在栈上,而可变长度字符串存储在堆上造成的,事实上确实与这点相关,如果要深入了解这点,请参阅rust深浅拷贝

这里提前给出结论,对于符合以下特征的的数据结构,它将一个已经拥有所有权的变量赋值给另一个变量时,会创建一个值的拷贝,而不是将所有权转移给新的变量:

  • 实现了”Copy”特质的类型。
  • 数据类型的大小固定在编译时已知,不引用堆上分配的内存。

“Copy”特质的基础类型包括像整数、浮点数、布尔值、字符、元组(只包含”Copy”类型的元素)等。这些类型在赋值时会自动复制数据,而不是转移所有权。这种行为大大方便了编程。


3. 引用和借用


3.1 引用和借用规则

显然,如果每要转移值的时候,如果都伴随着所有权的转移,对于我们程序的编写是十分不利的,因此有了引用和借用两种在不获取所有权的情况下访问值的类型。首先看一段代码,来理解什么是引用和借用,以及他们之间的区别:

// 引用
let mut i = 10;
let i1 = &i;
println!("{}", i1);
// 借用
let i2 = &mut i;    // here , i1'scope is over
*i2 = *i2 + 1;
println!("{}", i);
println!("{},{}", i1, i2);

简单通过代码及可看出,引用为不可变引用(immutable reference),借用为可变引用(mutable reference)。

当你把以上代码进行编译时,会发现如下报错:

error[E0502]: cannot borrow `i` as mutable because it is also borrowed as immutable
  --> src/main.rs:8:14
   |
5  |     let i1 = &i;
   |              -- immutable borrow occurs here
...
8  |     let i2 = &mut i;
   |              ^^^^^^ mutable borrow occurs here
...
11 |     println!("{},{}", i1, i2);
   |                       -- immutable borrow later used here

这里我们引入引用和借用的规则:

  • 借用不能有别名(Alias),也即借用只能有一个
  • 引用和借用不能够同时存在与一个作用域

这两条规则就相当于内存的读写锁,同一时刻,只能拥有一个写锁,或者多个多个读锁,且而这不能同时拥有,


3.2 引用和借用对所有者的影响

  • 引用的生命周期未结束时,所有者的所有权不可借出

3.3 引用与声明周期

参考资料

[1]. 官方文档