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 的所有权已转移
}
相关推荐
Nan_Shu_61414 分钟前
学习: Threejs (2)
前端·javascript·学习
G_G#22 分钟前
纯前端js插件实现同一浏览器控制只允许打开一个标签,处理session变更问题
前端·javascript·浏览器标签页通信·只允许一个标签页
@大迁世界37 分钟前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路1 小时前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug1 小时前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu121381 小时前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中1 小时前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路1 小时前
GDAL 实现矢量合并
前端
hxjhnct1 小时前
React useContext的缺陷
前端·react.js·前端框架
前端 贾公子2 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端