当前位置: 首页 > article >正文

Rust从入门到精通之进阶篇:11.所有权系统详解

所有权系统详解

所有权(Ownership)是 Rust 最独特的特性,它使 Rust 能够在不需要垃圾回收的情况下保证内存安全。在本章中,我们将深入探讨所有权系统的工作原理、借用规则和生命周期概念。

所有权规则回顾

首先,让我们回顾一下所有权的基本规则:

  1. Rust 中的每个值都有一个被称为其所有者的变量
  2. 值在任一时刻只能有一个所有者
  3. 当所有者离开作用域,这个值将被丢弃

这些简单的规则是 Rust 内存安全的基础。

所有权转移

当我们将一个值赋给另一个变量时,所有权会发生转移(move)。这与其他语言中的浅拷贝不同,因为原变量在转移后不能再使用。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // 所有权从 s1 转移到 s2
    
    // println!("s1: {}", s1); // 错误:s1 的值已被移动
    println!("s2: {}", s2); // 正常工作
}

深入理解移动语义

为什么 Rust 选择移动而不是复制?这与内存管理有关。考虑 String 类型的内存布局:

┌─────┬─────┬─────┐
│ ptr │ len │ cap │
└──┬──┴─────┴─────┘
   │
   v
┌───┬───┬───┬───┬───┬───┐
│ h │ e │ l │ l │ o │   │
└───┴───┴───┴───┴───┴───┘

如果简单地复制栈上的数据(ptr、len、cap),两个变量将指向同一块堆内存。当它们离开作用域时,会尝试释放同一内存两次,这会导致双重释放错误。

克隆

如果我们确实需要深度复制堆上的数据,可以使用 clone 方法:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // 深度复制
    
    println!("s1: {}", s1); // 正常工作
    println!("s2: {}", s2); // 正常工作
}

栈上数据的复制

对于存储在栈上的简单类型(如整数),复制的成本很低,所以它们默认实现了 Copy trait:

fn main() {
    let x = 5;
    let y = x; // x 被复制给 y,而不是移动
    
    println!("x: {}, y: {}", x, y); // 两者都可以使用
}

实现了 Copy trait 的类型包括:

  • 所有整数类型
  • 布尔类型
  • 浮点类型
  • 字符类型
  • 元组(当且仅当其包含的所有类型都是 Copy 的)

函数与所有权

将值传递给函数时,所有权规则同样适用:

fn main() {
    let s = String::from("hello");
    takes_ownership(s); // s 的所有权移动到函数内
    // println!("s: {}", s); // 错误:s 的值已被移动
    
    let x = 5;
    makes_copy(x); // x 是 Copy 类型,所以仍然可用
    println!("x: {}", x); // 正常工作
}

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
} // some_string 离开作用域并被丢弃

fn makes_copy(some_integer: i32) {
    println!("{}", some_integer);
} // some_integer 离开作用域,不会有特殊操作

返回值与所有权

函数返回值也会转移所有权:

fn main() {
    let s1 = gives_ownership(); // gives_ownership 将返回值移给 s1
    
    let s2 = String::from("hello");
    let s3 = takes_and_gives_back(s2); // s2 被移动到函数里,函数返回值移给 s3
} // s3 离开作用域被丢弃,s1 也是,s2 已被移走

fn gives_ownership() -> String {
    let some_string = String::from("yours");
    some_string // 返回 some_string,所有权移出函数
}

fn takes_and_gives_back(a_string: String) -> String {
    a_string // 返回 a_string,所有权移出函数
}

引用与借用

如果每次传递值都转移所有权,代码会变得非常繁琐。Rust 提供了引用机制,允许我们使用值而不获取所有权:

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // 传递 s1 的引用
    
    println!("'{}' 的长度是 {}", s1, len); // s1 仍然可用
}

fn calculate_length(s: &String) -> usize { // s 是 String 的引用
    s.len()
} // s 离开作用域,但它不拥有引用值的所有权,所以不会释放任何东西

创建引用的行为称为借用(borrowing)。引用默认是不可变的,如果我们尝试修改借用的值,会得到错误:

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

fn change(some_string: &String) {
    some_string.push_str(", world"); // 错误:不能修改借用的值
}

可变引用

如果需要修改借用的值,可以使用可变引用

fn main() {
    let mut s = String::from("hello");
    change(&mut s); // 传递可变引用
    println!("{}", s); // 输出 "hello, world"
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}
可变引用的限制

可变引用有一个重要限制:在特定作用域中,对于特定数据,只能有一个可变引用。这个限制防止了数据竞争:

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &mut s;
    // let r2 = &mut s; // 错误:不能同时有两个可变引用
    
    println!("{}", r1);
}

数据竞争发生在以下三种行为同时发生时:

  1. 两个或更多指针同时访问同一数据
  2. 至少有一个指针被用来写入数据
  3. 没有同步访问数据的机制

我们也不能同时拥有可变引用和不可变引用:

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s; // 没问题
    let r2 = &s; // 没问题
    // let r3 = &mut s; // 错误:不能在有不可变引用的同时使用可变引用
    
    println!("{}, {}", r1, r2);
    
    // r1 和 r2 在这里不再使用
    
    let r3 = &mut s; // 现在可以了
    println!("{}", r3);
}

悬垂引用

Rust 编译器确保引用永远不会变成悬垂引用(指向已被释放的内存):

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // 错误:返回指向已释放内存的引用
    let s = String::from("hello");
    &s // 返回 s 的引用,但 s 将在函数结束时离开作用域
} // s 离开作用域并被丢弃,其内存被释放

解决方案是直接返回 String

fn no_dangle() -> String {
    let s = String::from("hello");
    s // 返回 s 本身,所有权被移出函数
}

切片

切片(slice)是对集合中部分连续元素的引用。最常见的是字符串切片 &str

fn main() {
    let s = String::from("hello world");
    
    let hello = &s[0..5]; // 或 &s[..5]
    let world = &s[6..11]; // 或 &s[6..]
    let whole = &s[..]; // 整个字符串的切片
    
    println!("{}, {}, {}", hello, world, whole);
}

字符串字面值就是切片:

let s: &str = "Hello, world!";

这就是为什么字符串字面值是不可变的——&str 是不可变引用。

其他切片

除了字符串,其他集合也可以有切片:

fn main() {
    let a = [1, 2, 3, 4, 5];
    let slice = &a[1..3];
    
    println!("{:?}", slice); // 输出 [2, 3]
}

生命周期

生命周期是 Rust 引用有效性的另一个方面。每个引用都有一个生命周期,即引用保持有效的作用域。大多数情况下,生命周期是隐含的,但有时需要显式标注。

生命周期标注语法

生命周期标注以撇号(')开头,通常使用小写字母:

&i32        // 引用
&'a i32     // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用

函数中的生命周期标注

考虑一个返回两个字符串切片中较长者的函数:

// 不能编译
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

这段代码无法编译,因为 Rust 不知道返回的引用是来自 x 还是 y,所以不能确定其生命周期。我们需要添加生命周期标注:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

这告诉 Rust 返回的引用的生命周期与参数 xy 的生命周期中较短的那个相同。

结构体中的生命周期

如果结构体包含引用,也需要生命周期标注:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.')
        .next()
        .expect("Could not find a '.'");
    
    let i = ImportantExcerpt {
        part: first_sentence,
    };
    
    println!("{}", i.part);
}

生命周期省略规则

Rust 有一些生命周期省略规则,使得在某些情况下不需要显式标注:

  1. 每个引用参数都有自己的生命周期参数
  2. 如果只有一个输入生命周期参数,那么它被赋给所有输出生命周期参数
  3. 如果有多个输入生命周期参数,但其中一个是 &self&mut self,那么 self 的生命周期被赋给所有输出生命周期参数

静态生命周期

'static 是一个特殊的生命周期,表示引用在整个程序运行期间都有效:

let s: &'static str = "I have a static lifetime.";

字符串字面值被直接存储在程序的二进制文件中,所以它们总是可用的,因此具有 'static 生命周期。

所有权系统的高级应用

内部可变性

有时我们需要在拥有不可变引用的情况下修改数据。Rust 提供了 RefCell<T> 类型,它允许在运行时而不是编译时检查借用规则:

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(5);
    
    // 创建可变借用
    let mut mut_ref = data.borrow_mut();
    *mut_ref += 1;
    
    // 尝试同时创建不可变借用会导致运行时错误(panic)
    // let immut_ref = data.borrow(); // 这会导致 panic
    
    println!("{}", mut_ref);
    
    // 丢弃可变借用
    drop(mut_ref);
    
    // 现在可以创建不可变借用了
    let immut_ref = data.borrow();
    println!("{}", immut_ref);
}

所有权与并发

Rust 的所有权系统使得并发编程更加安全。例如,Arc<T> 提供了线程间安全共享的不可变访问:

use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(vec![1, 2, 3]);
    
    let mut handles = vec![];
    
    for i in 0..3 {
        let data_clone = Arc::clone(&data);
        let handle = thread::spawn(move || {
            println!("线程 {}: {:?}", i, *data_clone);
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
}

最佳实践

尽早返回所有权

如果函数不需要保留所有权,应该尽早返回:

// 不好的做法
fn process_data(data: Vec<i32>) -> Vec<i32> {
    // 处理数据
    data
}

// 好的做法
fn process_data(data: &[i32]) -> Vec<i32> {
    // 处理数据,返回新的 Vec
    data.to_vec()
}

使用克隆还是引用

在决定使用克隆还是引用时,考虑以下因素:

  • 性能要求:克隆可能会导致性能下降
  • 代码复杂性:过多的生命周期标注可能使代码难以理解
  • 数据大小:小数据类型(如整数)克隆的成本很低

使用 .into().to_owned() 转换所有权

fn main() {
    let s: &str = "hello";
    let owned_string: String = s.to_owned(); // 或 s.to_string() 或 String::from(s)
    
    let s: &str = "world";
    let owned_string: String = s.into(); // 使用 Into trait
    
    println!("{}, {}", s, owned_string);
}

练习题

  1. 编写一个函数,接受一个字符串切片,返回其中的第一个单词。如果字符串不包含空格,则返回整个字符串。

  2. 创建一个结构体 TextEditor,它包含一个文本字符串和当前光标位置。实现方法来插入文本、删除文本和移动光标。思考所有权和借用如何影响你的设计。

  3. 编写一个函数,接受两个向量作为参数,返回一个新向量,包含两个输入向量的所有元素。尝试使用引用实现,然后使用所有权转移实现,比较两种方法的优缺点。

  4. 创建一个 Cache 结构体,它可以存储一个计算结果和生成这个结果的函数。实现一个方法,如果缓存中已有结果则返回缓存的结果,否则调用函数计算结果并缓存。考虑如何处理函数的所有权和生命周期。

  5. 实现一个简单的链表数据结构,每个节点包含一个值和指向下一个节点的可选引用。思考如何处理节点的所有权和生命周期。

总结

在本章中,我们深入探讨了 Rust 的所有权系统:

  • 所有权规则和转移语义
  • 引用和借用机制
  • 可变性和借用规则
  • 切片类型
  • 生命周期标注和省略规则
  • 所有权系统的高级应用

所有权系统是 Rust 最独特和强大的特性,它使 Rust 能够在不需要垃圾回收的情况下保证内存安全。掌握所有权系统是成为熟练的 Rust 开发者的关键。在接下来的章节中,我们将探索更多 Rust 的高级特性,如泛型、特质和高级类型系统。


http://www.kler.cn/a/611215.html

相关文章:

  • 第十一节 MATLAB关系运算符
  • 电动自行车/电动工具锂电池PCM方案--SH367003、SH367004、SH79F329
  • 深度分页优化思路
  • C++ 多线程简要讲解
  • Modbus RTU ---> Modbus TCP透传技术实现(Modbus透传、RS485透传、RTU透传)分站代码实现、协议转换器
  • Postman 下载文件指南:如何请求 Excel/PDF 文件?
  • 2025BAT大厂Java面试2000题精选(附答案+考点分析)
  • 人员进出新视界:视觉分析算法的力量
  • 淘宝获取商品sku详情API接口如何调用?
  • 前端学习笔记--CSS
  • vue vue3 走马灯Carousel
  • 如何 编译 px4
  • 物理环境与安全
  • 第十四届蓝桥杯大赛软件赛省赛C/C++ 大学 B 组(部分题解)
  • 业务相关
  • 大模型开发框架LangChain GO
  • 5.Excel:从网上获取数据
  • macbook电脑如何清理键盘防止误触
  • 脑启发式AI Agent:解锁人类大脑奥秘,迈向真正的通用人工智能(AGI)
  • AIDD-人工智能药物设计-计算驱动的药物再定位研究:策略、工具评测与典型案例分析