Rust 所有权理解与运用

【本文正在参加金石计划附加挑战赛------第三期命题】

前言

Rust 的所有权系统是其最独特且区分于其他语言的特性之一,对语言的其余部分具有深远的影响。这一机制使得 Rust 能够在没有垃圾回收器的情况下确保内存安全,因此,深入了解 Rust 中的所有权运作方式至关重要。

所有权概述

所有权(ownership)是 Rust 进行管理内存的规则。

  • 在某些编程语言中,配备了垃圾回收机制,该机制能够在程序执行过程中定期识别并清理不再使用的内存资源(例如js的垃圾回收机制)
  • 而在另外一些语言里,则需要程序员自行负责内存的分配与释放 (例如C语言的malloc和free)

Rust : 每个值都由一个唯一 的所有者管理。一旦所有者超出其作用域,该值即会被释放。通过这样的所有权机制,Rust 能够保证内存资源的安全性和高效利用。

Rust作用域示例

rust 复制代码
fn main() { 
    { 
        // s 在这里无效,它尚未声明 
        let s = "hello"; // 从此处起,s 是有效的
        // 使用 s 
    } 
// 此作用域已结束,s 不再有效 
}

所有权规则

1. 转移所有权

示例:a和b是否进行了所有权转移?

rust 复制代码
let a = 5; 
let b = a;

这段代码并没有发生所有权的转移

原因: 代码首先将数值 5 绑定至变量 a,随后将 a 的值复制给 b。最终,a 和 b 的值均为 5。由于整数属于 Rust 的基本数据类型,它们是具有固定大小的简单值,因此 a 和 b 的值是通过自动复制的方式进行赋值的,并且这些值都存储在栈中,无需在堆上分配内存。

整个过程中的赋值都是通过值拷贝的方式完成(发生在栈中),因此并不需要所有权转移。

示例:s1与s2是否发生所有权转移?

rust 复制代码
let s1 = String::from("hello"); 
let s2 = s1;
// println!("{}", s1);
println!("{}", s2); // 正常打印 "hello"

发生了所有权转移;

String 类型是一个复杂类型,指向了一个堆上的空间,这里存储着它的真实数据。

如果此时打印s1会发生什么呢?由于 Rust 禁止你使用无效的引用,你会看到以下的错误:

text 复制代码
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("{}, world!", 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
  |
3 |     let s2 = s1.clone();
  |                ++++++++

For more information about this error, try `rustc --explain E0382`.

在其他编程语言中,可能遇到过"浅拷贝"(shallow copy)和"深拷贝"(deep copy)的概念。当提到仅复制指针、长度和容量而没有复制实际的数据时,这听起来很像是浅拷贝的过程。然而,在 Rust 中,由于同时使原始变量(如 s1)失效,这一过程被特称为"移动"(move),而非简单的浅拷贝。这意味着,当我们将 s1 的值赋给 s2 时,实际上是将 s1 的所有权转移给了 s2,并且 s1 不再有效。

2.克隆

rust 复制代码
let s1 = String::from("hello"); 
let s2 = s1.clone(); 
println!("s1 = {}, s2 = {}", s1, s2); // 正常打印 "hello hello"

3.函数传值与返回

基本数据类型 传值入函数不会发生所有权转移,原因是栈上数据拷贝,不涉及堆数据

rust 复制代码
fn main() {
    let s = String::from("hello");  // s 进入作用域

    takes_ownership(s);             // s 的值移动到函数里 所以到这里不再有效
                                    
    println!("在move进函数后继续使用s: {}",s); // 报错
    
    let x = 5;                      // x 进入作用域

    makes_copy(x);                  // x 应该移动函数里,
                                    // 但 i32 是 Copy 的,所以在后面可继续使用 x
    println!("在move进函数后继续使用x: {}",x); // 打印5

} // 这里, 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 移出作用域。不会有特殊操作
rust 复制代码
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("hello"); // 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 并移出给调用的函数
}

4.引用与借用

Rust 提供了一种机制,允许像其他编程语言那样使用变量的指针或引用,这就是所谓的"借用"(Borrowing)。

在 Rust 中,借用是指获取一个变量的引用。这就好比在现实生活中,当你需要使用某人拥有的物品时,可以向他们借来使用。使用完毕之后,你必须将物品归还给原主人。同样地,在 Rust 中,当你借用一个变量时,实际上是在创建该变量的一个临时引用。使用完毕后,这个引用会自动失效,从而确保原变量的所有权不会受到影响。

1.引用与解引用

rust 复制代码
fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
    assert_eq!(5, y); //不允许比较整数与引用,因为它们是不同的类型。必须使用解引用运算符解出引用所指向的值。
}

变量 x 存储了一个 i32 类型的值 5。y 则是 x 的一个引用。我们可以断定 x 的值为 5。 若要对 y 的值进行断言,则需要使用 *y 来解除引用,以获取 y 所指向的实际值(即解引用)。完成解引用后,就能访问 y 指向的整数值,并能够将其与 5 进行比较。

简而言之,通过 *y 我们可以从 y 引用中提取出 x 的值,进而验证其是否等于 5。这样做的原因是 y 本身只是一个指向 x 的指针,而 *y 表达式告诉编译器我们想要访问的是 y 指向的具体值。

2.不可变引用

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

rust 复制代码
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();
    s.push_str(", world"); // 报错,正如变量默认不可变一样,引用指向的值默认也是不可变的
}
  1. 无需像上章一样:先通过函数参数传入所有权,然后再通过函数返回来传出所有权,代码更加简洁
  2. calculate_length 的参数 s 类型从 String 变为 &String

这里的& 符号即是引用,允许你使用值,但是不能获取其所有权

3.可变引用

首先,声明 s 是可变类型,其次创建一个可变的引用 &mut s 和接受可变引用参数 some_string: &mut String 的函数

rust 复制代码
fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}
  • 可变引用同时只能存在一个
  • 可变引用与不可变引用不能同时存在

4.借用规则总结

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

所有权应用场景总结

1. 防止数据竞争 (Data Race Prevention)

在并发编程中,多个线程可能访问相同的资源,容易产生数据竞争。Rust 的所有权规则结合借用检查,确保在编译时检测出不安全的并发访问。

示例:多线程并发访问

  • Arc 管理共享所有权,允许多线程安全共享数据。

  • Mutex 确保同一时间只有一个线程访问数据。

  • Rust 的所有权系统确保线程安全,避免数据竞争。

rust 复制代码
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // 使用 Arc 和 Mutex 来管理共享状态
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

2. 自动释放资源 (RAII)

Rust 的所有权和作用域规则使得资源的分配和释放自动化。文件、网络连接等资源在超出作用域时会自动释放,无需手动管理。

示例:文件操作

  • file 超出作用域时,Rust 会调用 drop 自动关闭文件。

  • 这种自动释放避免了资源泄露。

rust 复制代码
use std::fs::File;
use std::io::{self, Write};

fn main() -> io::Result<()> {
    let file_path = "example.txt";

    // 创建文件并写入内容
    {
        let mut file = File::create(file_path)?;
        writeln!(file, "Hello, Rust ownership!")?;
        // 文件自动关闭:所有权离开作用域时触发 drop
    }

    // 文件可以重新打开进行其他操作
    let content = std::fs::read_to_string(file_path)?;
    println!("File content: {}", content);

    Ok(())
}

3.零开销抽象 (Zero-Cost Abstraction)

所有权和生命周期结合,能高效地管理复杂数据结构而不增加运行时开销。

示例:使用 Vector 管理数据

rust 复制代码
fn main() {
    let mut numbers = vec![1, 2, 3];
    for number in &numbers {
        println!("{}", number);
    }

    numbers.push(4); // 所有权仍然属于 numbers,允许修改
    println!("{:?}", numbers);
}

4.高效跨函数传递数据

通过所有权转移或借用,可以高效传递和操作大数据。

rust 复制代码
fn process_data(data: String) -> usize {
    println!("Processing data: {}", data);
    data.len() // 数据的所有权在函数内使用完毕后自动释放
}

fn main() {
    let data = String::from("Hello, Rust!");
    let length = process_data(data); // 所有权转移给 process_data
    println!("Data length: {}", length);

    // println!("{}", data); // 编译错误,data 的所有权已转移
}
相关推荐
code_shenbing2 小时前
跨平台WPF框架Avalonia教程 三
前端·microsoft·ui·c#·wpf·跨平台·界面设计
白臻2 小时前
使用element-plus el-table中使用el-image层级冲突table表格会覆盖预览的图片等问题
前端·vue.js·elementui
北极糊的狐2 小时前
vue使用List.forEach遍历集合元素
前端·javascript·vue.js
晓看天色*2 小时前
[JAVA]MyBatis框架—获取SqlSession对象
java·开发语言·前端
ZVAyIVqt0UFji3 小时前
Reactflow图形库结合Dagre算法实现函数资源关系图
开发语言·前端·javascript·ecmascript
luckilyil3 小时前
前端—Cursor编辑器
前端·编辑器
cooldream20093 小时前
快速上手 Vue 3 的高效组件库Element Plus
前端·javascript·vue.js·element plus
我是苏苏3 小时前
Web开发:ORM框架之使用Freesql的DbFrist封装常见功能
java·前端·jvm
疯狂的沙粒4 小时前
Vue项目开发 vue实例挂载的过程?
前端·javascript·vue.js
吃葡萄不吐葡萄皮嘻嘻4 小时前
el-table实现最后一行合计功能并合并指定单元格
前端·vue.js·elementui