所有权介绍

什么是所有权

在Rust编程语言中,所有权(Ownership)是一个核心概念,它帮助Rust在编译时管理内存,确保内存安全且无需垃圾回收机制。所有权规则主要有以下几点:

1. 每一个值都有一个所有者(Owner)

在Rust中,每一个值在某一时刻都有且仅有一个所有者。

所有者是一个变量,该变量在作用域结束时释放其所拥有的值。

2. 值在赋值时被移动(Move)

当一个值被赋给一个新变量时,该值会被移动,其所有权会从旧变量转移到新变量。

这意味着旧变量在赋值后将不再拥有该值,尝试使用它会导致编译错误。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    // 此处 s1 已经失效,避免二次释放内存
    println!("{}", s1);
}

3. 所有权和作用域(Scope)

当所有者变量离开其作用域时,Rust会自动释放其所拥有的内存。

作用域是由大括号{}定义的代码块。所有权被移动示例 :

fn main() {
    let s1 = String::from("Hello");
    let len:i32 = s_len(s1);
    // 此处 s1 所有权已经被移动了,不能再使用
    println!("The length of {} is {}",s1,len);
}

fn s_len(s:String)->i32{
    s.len() as i32
}

利用引用保持所有权的示例 :

fn main() {
    let s1 = String::from("Hello");
    let len:i32 = s_len(&s1);
    println!("The length of {} is {}",s1,len);
}

fn s_len(s:&String)->i32{
    s.len() as i32
}

4.引用与借用

如果仅仅支持通过转移所有权的方式获取一个值,那会让程序变得复杂。 Rust 能否像其它编程语言一样,使用某个变量的指针或者引用呢?答案是可以。

Rust 通过 借用(Borrowing) 这个概念来达成上述的目的,获取变量的引用,称之为借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来,当使用完毕后,也必须要物归原主。

引用与解引用

常规引用是一个指针类型,指向了对象存储的内存地址。在下面代码中,我们创建一个 i32 值的引用 y,然后使用解引用运算符来解出 y 所使用的值:

fn main() {
    let x = 5;
    let y = &x;
    assert_eq!(5, x);
    assert_eq!(5, *y);
}

变量 x 存放了一个 i32 值 5。y 是 x 的一个引用。可以断言 x 等于 5。

然而,如果希望对 y 的值做出断言,必须使用 *y 来解出引用所指向的值(也就是解引用)。

一旦解引用了 y,就可以访问 y 所指向的整型值并可以与 5 做比较。

相反如果尝试编写 assert_eq!(5, y);,则会得到如下编译错误:

error[E0277]: can't compare `{integer}` with `&{integer}`
 --> src/main.rs:6:5
  |
6 |     assert_eq!(5, y);
  |     ^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}` // 无法比较整数类型和引用类型
  |
  = help: the trait `std::cmp::PartialEq<&{integer}>` is not implemented for
  `{integer}`

不允许比较整数与引用,因为它们是不同的类型。必须使用解引用运算符解出引用所指向的值。

不可变引用

下面的代码,我们用 s1 的引用作为参数传递给 calculate_length 函数,而不是把 s1 的所有权转移给该函数:

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
    s.len()
}

能注意到两点:

无需像上章一样:先通过函数参数传入所有权,然后再通过函数返回来传出所有权,代码更加简洁

calculate_length 的参数 s 类型从 String 变为 &String

这里,& 符号即是引用,它们允许你使用值,但是不获取所有权。

同理,函数 calculate_length 使用 & 来表明参数 s 的类型是一个引用:

fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
    s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,

可变引用

我们来看下面的示例 :

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

上面的代码是错误的:

首先 s 不是一个可变变量,我们进行修改:

let mut s: String = String::from("hello");

依据存在错误 : &s 不是一个可变引用,我们将其声明为可变引用 :

fn main() {
    let mut s: String = String::from("hello");
    change(&mut s);
    println!("{}", s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

通过 &mut 语法可以将一个引用升级为可变引用,实现修改变量值的目的。

可变引用与不可变引用不能同时存在

下面的代码会导致一个错误:

let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题
println!("{}, {}, and {}", r1, r2, r3);

错误如下:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 // 无法借用可变 `s` 因为它已经被借用了不可变
 --> src/main.rs:6:14  |
4 |     let r1 = &s; // 没问题
  |              -- immutable borrow occurs here 不可变借用发生在这里
5 |     let r2 = &s; // 没问题
6 |     let r3 = &mut s; // 大问题
  |              ^^^^^^ mutable borrow occurs here 可变借用发生在这里
7 |
8 |     println!("{}, {}, and {}", r1, r2, r3);
  |                                -- immutable borrow later used here 不可变借用在这里使用

其实这个也很好理解,正在借用不可变引用的用户,肯定不希望他借用的东西,被另外一个人莫名其妙改变了。

多个不可变借用被允许是因为没有人会去试图修改数据,每个人都只读这一份数据而不做修改,因此不用担心数据被污染。

注意:

引用的作用域 s 从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号 }

5. 悬挂引用(Dangling References)

Rust通过所有权系统确保不会有悬挂引用,即不会有指向已经被释放的内存的引用。

一旦所有者变量离开其作用域,其所拥有的值会被立即释放,任何指向该值的引用都会变得无效。

悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。在 Rust 中编译器可以确保引用永远也不会变成悬垂状态:当你获取数据的引用后,编译器可以确保数据不会在引用结束前被释放,要想释放数据,必须先停止其引用的使用。

让我们尝试创建一个悬垂引用,Rust 会抛出一个编译时错误:

fn main() {
    let reference_to_nothing = dangle();
}
fn dangle() -> &String {
    let s = String::from("hello");
    &s
}

这里是错误:

error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
5 | fn dangle() -> &'static String {
  |                ~~~~~~~~

错误信息引用了一个我们还未介绍的功能:生命周期(lifetimes)。不过,即使你不理解生命周期,也可以通过错误信息知道这段代码错误的关键信息:

该函数返回了一个借用的值,但是已经找不到它所借用值的来源

仔细看看 dangle 代码的每一步到底发生了什么:

fn dangle() -> &String { // dangle 返回一个字符串的引用
    let s = String::from("hello"); // s 是一个新字符串
    &s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
// 危险!

因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s 将被释放,但是此时我们又尝试去返回它的引用。这意味着这个引用会指向一个无效的 String,这可不对!

其中一个很好的解决方法是直接返回 String:

fn no_dangle() -> String {
    let s = String::from("hello");
    s
}
这样就没有任何错误了,最终 String 的 所有权被转移给外面的调用者。

6. 复制类型与移动类型

Rust中的某些类型实现了Copy trait,这些类型在赋值时会被复制而不是移动。

例如,基本数据类型(如整数和浮点数)和小型结构体通常实现了Copy trait。

对于没有实现Copy trait的类型(如大多数集合类型和字符串),赋值会导致值的移动。


fn main() {
    let x = 5;
    let y = 10;
    // 整数类型是固定长度的,并且编译器知道它们占用多少空间。
    // 可以将数据完整的存储在栈上,而不必分配在堆上。
    // 对应类似的变量类型 rust 实现了 Copy trait ,可以复制
    // 标量类型都实现了 Copy trait ,所以不需要手动实现
    // 赋值操作符是等号,而不是箭头
    println!("The value of x is: {}", x);
    println!("The value of y is: {}", y);
}

总结

通过所有权系统,Rust能够在编译时捕获许多常见的内存错误,如使用悬挂指针和解引用空指针,从而提高了程序的稳定性和安全性。

借用规则总结

总的来说,借用规则如下:

同一时刻,你只能拥有要么一个可变引用,要么任意多个不可变引用,引用必须总是有效的。