likes
comments
collection
share

四、 所有权和借用

作者站长头像
站长
· 阅读数 5

在其他编程语言「go、java」中,都有一套自己的垃圾回收器来保证程序的内存安全。然而,存在垃圾回收器的语言有一个缺点就是垃圾回收会带来一定的开销,最大的问题就是存在 Stop The Word,有些高性能场景是无法接受的。对于 c 和 c++ 来说,不存在垃圾回收的问题,但是需要开发者手动管理内存,增加了开发成本。

Rust 通过所有权系统,保证变量离开作用域后,自用释放内存,即解决了需要垃圾回收器的问题,也解决了开发者手动管理内存问题「本质上还是通过编译器来解决内存管理问题」。当然,带来的问题就是编译时长增加,同时需要理解这一套所有权机制。所有权系统要解决的问题是何时释放内存。

所有权

先介绍一下 Rust 所有权的规则,通过所有权规则来实现对内存的有效管理:

  • Rust 中每一个值都有一个被称为所有者的变量
  • 值在任意时刻有且只有一个所有者
  • 当所有者离开作用域,这个值将被丢弃

Rust 所有权规则本质上就是为了解决如何对堆内存「栈内存无需进行处理,程序运行过程中会进行进栈和出栈操作」进行及时回收的问题?在 Rust 中,String 里的字符串是存储在堆上的。看下面这个 case,print s1 会报错,这是由于值 zjl 的所有权由 s1 转移到 s2s1 无法被继续使用。从而保证了值 "zjl" 在任意时刻只有一个所有者。因此,可以得知,Rust 先保证堆内存中值只有一个所有者来解决被重复回收的问题;通过所有者的作用域来解决什么时候回收的问题。

fn main() {
    // 分配在堆上,需要进行回收,值 "zjl" 的所有者为 s1
let s1 = String::from("zjl");
    // 所有权发生移动, 值的所有者变为 s2
let s2 = s1;
     // println!("{}{}", s1, s2); ^^ value borrowed here after move
println!("{}", s2);
    // 值的所有者离开作用域,值被丢弃
}

四、 所有权和借用

对于标量类型,Rust 直接会 copy 值到栈上,所以下面的代码是可以执行的。Rust 中有一个叫做 Copy trait 的标注,如果一个类型实现了 Copy trait,那么一个旧的变量在赋值给其他变量后仍可以使用。在 Rust 中,实验 Copy trait 的类型有:所有整型、布尔类型、浮点类型、字符类型、元组「当且仅当其包含的类型也都实现了 Copy trait」。

fn main() {
    // 值:8, 所有者:s1
let s1 = 8;
    // copy 一个值 8 到栈上, 所有者:s2
let s2 = s1;
    // 栈中有两个值,对应两个所有者 s1, s2
    println!("{}{}", s1, s2);
}

既然对于标量类型,变量赋值会重新在栈上 Copy 一个值。那对于分配到堆上的类型,我们也可以深拷贝的形式,在堆上新分配一块内存,这样就可以保证两个变量分别持有对应的内存块。当然,这样做的缺点就是造成内存的分配和销毁,影响性能。

fn main() {
    // 分配在堆上,需要进行回收,值 "zjl" 的所有者为 s1
let s1 = String::from("zjl");
    // 所有权发生移动, 值的所有者变为 s2
let s2 = s1.clone();
    println!("{}{}", s1, s2);
}

四、 所有权和借用

对于函数的入参和出参,同样会发生所有权转移。

fn main() {
    let s = String::from("zjl");
    take_ownership(s);
    // println!("{}", s); s 的所有权已经被转移,无法打印 s

let s = give_ownership(); // 获取函数返回值的所有权
println!("{}", s);
}

// 函数参数赋值也会发生所有权转移, 所有权移动到函数变量 param
fn take_ownership(param: String) {
    println!("{}", param);
} // 函数结束,param 移出作用域,调用 drop 方法,回收内存

// 将所有权移交给函数的返回结果上
fn give_ownership() -> String {
    let s = String::from("zjl"); // 进入作用域
s // 所有权移交
}

引用和借用

下面是一个计算 String 长度并打印的例子,由于所有权会发生移动,当 String 作为函数入参时,函数调用完成后无法继续使用 String。这里我们可以 clone 一份 s,但是会造成性能损失,那有没有不 clone 的方案呢?

fn main() {
    let s = String::from("hello world");
    let len = calculate_len_without_ref(s);
    println!("str: {}, len: {}", s, len) // ^ value borrowed here after move, s 的所有权已经移交到 calculate_len_without_ref, 因此这里无法使用
}

fn calculate_len_without_ref(s: String) -> usize {
    s.len()
}

在 Rust 中使用 & 即表示引用,允许你使用值但不获取其所有权。在下面的代码中,函数 calculate_len 声明了参数类型 &String,为引用类型。而创建一个引用的行为称为借用。第三行函数调用时创建一个 s1 的引用,值 "hello word" 的所有者仍然属于 s1,因此可以继续使用 s1。

fn main() {
    let s1 = String::from("hello world");
    let len = calculate_len(&s1);
    println!("str: {}, len: {}", s, len) 
}

fn calculate_len(s: &String) -> usize {
    s.len()
}

四、 所有权和借用

引用又分为可变引用和不可变引用。当需要修改借用过来的变量时,则需要创建一个可变引用。引用的规则为: 任意时刻,要么只有一个可变引用,要么只能有多个不可变应用;引用必须是有效的。

fn main() {
    let mut s = String::from("hello world");
    // 借用变量 s, 声明一个可变引用 s1
let s1 = &mut s;
    // 修改引用的值 s
append(s1);
    println!("str: {}", s);
    // 声明不可变引用 s2,s3
    let (s2, s3) = (&s, &s);
    // println!("s1: {}, s2: {}", s1, s2); immutable borrow occurs here, 同时访问了可变引用 s1 和不可变引用 s2, 这是不允许的, 主要是保证数据安全,不允许同时处理可变引用和不可变引用
println!("s2: {}, s3: {}", s2, s3); // 可以编译通过, 已经超出了 s1 的作用域,可以同时处理多个不变引用

 // s.push_str("end"); cannot borrow `s` as mutable because it is also borrowed as immutable, 下面访问了不可变引用, 因此不能修改值的所有者 s, 即不能作为 mutable 处理
println!("s: {}, s2: {}", s, s2);
}

所有权解决的是内存安全问题,即保证堆上的每块内存都有对应的所有者,当所有者离开作用域之后,回收内存。引用和借用解决的是数据访问的问题,如何在不破坏所有权规则的条件下,方便快捷的访问变量。基于此,又有了可变引用和不可变引用规则,解决引用变量的可修改问题。