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

Rust所有权(下):引用、借用与切片

引言

在上一章中,我们深入探讨了Rust的所有权系统,理解了其核心规则:单一所有者、所有权的移动(Move)以及CopyClone trait。我们看到了所有权如何在函数调用和返回时进行转移,例如takes_and_gives_back函数:

fn takes_and_gives_back(a_string: String) -> String {a_string
}

虽然这种模式保证了内存安全,但在实际编程中却显得相当笨拙。如果我们仅仅是想让一个函数读取或修改某个数据,而并不想交出其所有权,难道每次都必须像这样把所有权传来传去吗?这不仅使代码变得冗长,还可能带来不必要的性能开销。

幸运的是,Rust提供了一个优雅的解决方案来应对这一挑战:借用(Borrowing)。通过“借用”,我们可以在不转移所有权的前提下,允许代码的其他部分临时“使用”一个值。实现借用的机制就是引用(References)

本章将带你深入理解引用和借用这两个紧密相连的概念。我们将学习如何创建不可变引用和可变引用,并探索Rust编译器为了保证安全而施加的“借用规则”。最后,我们将介绍一个基于引用机制的强大抽象——切片(Slices),它为我们提供了一种安全、高效的方式来引用集合的一部分。

掌握了借用和切片,你将能够编写出既符合所有权规则,又兼具灵活性和高效性的Rust代码,真正解锁Rust语言的强大潜力。


1. 引用(References)与借用(Borrowing)

引用允许你引用(refer to)某个值,而无需取得其所有权。它就像一个指针,存储的是另一个值的地址,但与C/C++中的指针不同,Rust的引用在编译时受到了借用检查器(borrow checker)的严格审查,以确保其始终指向有效的内存。

通过引用来访问值的过程,我们称之为借用(Borrowing)

1.1 创建不可变引用

我们可以使用&操作符来创建一个值的引用。默认情况下,引用是不可变的,意味着你不能通过这个引用来修改它所指向的值。

// File: src/main.rs
// Rust Version: 1.73.0fn main() {let s1 = String::from("hello");// calculate_length 函数“借用”了 s1,而不是取得其所有权let len = calculate_length(&s1);println!("The length of '{}' is {}.", s1, len); // s1 在这里仍然是有效的
}// a_string 的类型是 &String,表示它是一个指向 String 的引用
fn calculate_length(a_string: &String) -> usize {a_string.len()
} // a_string 在这里离开作用域。但因为它不拥有所指向的值,// 所以当它被销毁时,什么也不会发生。s1 的数据不会被 drop。

代码与内存分析:

  1. let s1 = String::from("hello");:和之前一样,s1在栈上,拥有堆上的字符串数据。
  2. calculate_length(&s1);
    • &s1创建了一个指向s1所拥有数据的引用。这个引用本身(一个地址)被压入calculate_length函数的栈帧。
    • s1的所有权没有被移动。它仍然是堆上数据的所有者。
    • calculate_length函数的参数a_string的类型是&String。它“借用”了s1
  3. calculate_length函数内部,我们可以通过a_string来访问String的数据(例如调用.len()方法),但我们不能修改它。
  4. calculate_length函数结束时,a_string这个引用被销毁。因为它不拥有数据,所以堆上的字符串数据安然无恙。
  5. 回到main函数,s1依然是有效的所有者,我们可以继续使用它。

图示:借用时的内存布局

         main's STACK                         HEAP
+----------------------+
| s1                   |
|   ptr   ------------+------------------->+-----------+
|   len   = 5          |                    | h | e | l | l | o |
|   capacity = 5       |                    +-----------+
+----------------------+|V  调用 calculate_length(&s1)calculate_length's STACK
+----------------------+
| a_string (&String)   |
|   (address of s1's data) --+
+----------------------+     ||main's STACK          |
+----------------------+     |
| s1                   |     |
|   ptr   ------------+------+
|   ...                |
+----------------------+

通过借用,我们完美地解决了之前的问题。函数可以“观察”数据,而无需承担管理的责任。

1.2 创建可变引用

如果我们确实需要在一个函数中修改被借用的值,该怎么办?Rust允许我们创建可变引用(mutable references),使用&mut语法。

// File: src/main.rs
// Rust Version: 1.73.0fn main() {let mut s = String::from("hello"); // s 必须是可变的change(&mut s);println!("{}", s);
}fn change(some_string: &mut String) {some_string.push_str(", world");
}

代码分析:

  1. let mut s = ...:首先,被借用的值s本身必须被声明为mut。如果你想通过可变引用来修改它,那么它自身必须是可变的。
  2. change(&mut s):我们使用&mut s来创建一个指向s的可变引用。
  3. fn change(some_string: &mut String):函数的参数类型现在是&mut String,表示它接收一个可变的String引用。
  4. some_string.push_str(...):在函数内部,我们可以通过这个可变引用来调用会修改String的方法。

输出结果:

hello, world

可变引用为我们提供了强大的灵活性,但“能力越大,责任越大”。为了防止数据竞争(data races)等并发问题,Rust对可变借用施加了非常严格的限制。这正是我们要探讨的借用规则。


2. 借用的规则

借用机制的核心是编译器在编译时强制执行的一套规则。这些规则旨在从根本上防止数据竞争。数据竞争通常在以下三个条件同时满足时发生:

  • 两个或更多的指针(或引用)同时访问同一块数据。
  • 其中至少有一个指针(或引用)被用来写入数据。
  • 没有使用任何同步机制来控制对数据的访问。

Rust的借用规则通过确保这种情况在编译时就不可能发生,从而保证了线程安全。

借用规则如下:

  1. 在任何给定的作用域内,对于一块数据,你只能拥有以下两者之一
    • 一个可变引用 (&mut T)。
    • 任意数量的不可变引用 (&T)。
  2. 引用必须始终有效(valid)。

让我们通过例子来深入理解第一条规则,这也是最核心的一条。

2.1 规则:一个可变引用 vs. 多个不可变引用

场景一:同时存在多个不可变引用(合法)
// File: src/main.rs
// Rust Version: 1.73.0fn main() {let s = String::from("hello");let r1 = &s; // OKlet r2 = &s; // OKprintln!("r1 = {}, r2 = {}", r1, r2);// r1 和 r2 在这里之后不再被使用
}

这是完全合法的。我们可以创建任意多个对s的不可变引用。因为它们都只是读取数据,所以不会产生任何冲突。

场景二:一个可变引用(合法)
// File: src/main.rs
// Rust Version: 1.73.0fn main() {let mut s = String::from("hello");let r1 = &mut s; // OKprintln!("r1 = {}", r1);
}

这也是合法的。我们创建了一个可变引用。

场景三:一个可变引用与一个不可变引用共存(非法)
// File: src/main.rs
// Rust Version: 1.73.0fn main() {let mut s = String::from("hello");let r1 = &s; // 不可变借用let r2 = &mut s; // 可变借用// 下面这行代码将无法编译!// println!("r1 = {}, r2 = {}", r1, r2); // error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
}

为什么这是非法的?

想象一下,如果这段代码可以编译。r1期望它指向的数据s是不会改变的。但r2却可能随时修改s的内容。这就可能导致r1指向的数据突然变得无效或不一致,这是一种非常危险的状态,尤其是在多线程环境下。Rust编译器通过在编译时禁止这种情况来保护我们。

场景四:两个可变引用共存(非法)
// File: src/main.rs
// Rust Version: 1.73.0fn main() {let mut s = String::from("hello");let r1 = &mut s;let r2 = &mut s; // 第二个可变借用// 下面这行代码将无法编译!// println!("r1 = {}, r2 = {}", r1, r2);// error[E0499]: cannot borrow `s` as mutable more than once at a time
}

这也是非法的,原因与数据竞争的定义直接相关。如果r1r2都可以修改s,它们可能会在没有同步的情况下互相干扰,导致数据损坏。

2.2 引用的作用域与非词法作用域生命周期(NLL)

一个引用的作用域从它被创建开始,一直持续到它最后一次被使用的地方。这一点非常重要,它得益于Rust编译器的一项高级特性——非词法作用域生命周期(Non-Lexical Lifetimes, NLL)。

在旧版本的Rust中,引用的作用域会持续到其所在的花括号{}结束。但在现代Rust中(自2018版起),编译器足够智能,能够精确地分析出引用最后一次被使用的位置。

这使得一些在逻辑上安全的代码现在可以被接受了:

// File: src/main.rs
// Rust Version: 1.73.0fn main() {let mut s = String::from("hello");let r1 = &s; // 不可变借用开始let r2 = &s; // 不可变借用开始println!("r1 = {} and r2 = {}", r1, r2);// r1 和 r2 的作用域在这里结束,因为它们之后再也没有被使用let r3 = &mut s; // OK!现在可以进行可变借用了println!("r3 = {}", r3);
}

代码分析:

  • r1r2是不可变引用。它们最后一次被使用是在第一个println!宏中。
  • 编译器分析出,在let r3 = &mut s;这一行,r1r2已经不再活跃(no longer live)。
  • 因此,此时对s进行可变借用是安全的,因为不存在任何活跃的不可变引用与之冲突。

这个特性极大地提升了Rust编程的灵活性和人体工程学,使得借用规则在实践中更加合理。


3. 悬垂引用(Dangling References)

借用规则的第二条是“引用必须始终有效”。这意味着Rust编译器会保证你永远不会持有一个指向无效内存的引用。这种引用在其他语言中被称为悬垂指针(dangling pointer),是导致程序崩溃和安全漏洞的常见原因。

考虑以下在C/C++中很常见的错误模式:

// C 语言中的悬垂指针例子
char* dangle() {char s[] = "hello"; // s 是在 dangle 函数的栈上分配的return s; // 返回一个指向 s 的指针
} // 函数结束,s 的内存被释放,返回的指针现在指向无效内存

Rust编译器会如何处理类似的情况?

// File: src/main.rs
// Rust Version: 1.73.0fn main() {let reference_to_nothing = dangle();
}fn dangle() -> &String { // dangle 返回一个 String 的引用let s = String::from("hello"); // s 是 dangle 函数内部的局部变量&s // 我们尝试返回对 s 的引用
} // s 在这里离开作用域,其拥有的内存被释放// 我们返回的引用将指向一块被释放的内存

这段代码无法通过编译。Rust编译器会给出一个非常明确的错误信息:

error[E0106]: missing lifetime specifier--> src/main.rs:7:16|
7 | 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|
7 | fn dangle() -> &'static String {|                ~~~~~~~error[E0515]: cannot return reference to local variable `s`--> src/main.rs:10:5|
10 |     &s|     ^^ returns a reference to data owned by the current function

编译器的错误信息非常清晰:

  1. cannot return reference to local variable 's':你不能返回一个指向局部变量的引用。
  2. returns a reference to data owned by the current function:因为这个局部变量s在函数结束时就会被销毁,返回的引用会立即失效。

为了解决这个问题,我们应该直接返回String本身,将所有权转移出去:

// File: src/main.rs
// Rust Version: 1.73.0fn main() {let s = no_dangle();println!("Got string: {}", s);
}fn no_dangle() -> String {let s = String::from("hello");s // 直接移动 s 的所有权
}

通过这种方式,Rust的借用检查器和所有权系统协同工作,在编译时就彻底根除了悬垂引用的可能性,这是Rust提供核心内存安全保证的关键所在。


4. 切片(Slices)

切片是另一种不持有所有权的数据类型。它允许你引用一个集合中连续的一部分元素,而不是整个集合。切片本身也是一个“胖指针”,它存储了指向起始元素的指针和切片的长度。

4.1 字符串切片(String Slices)

字符串切片是对String中一部分的引用。它的类型写作&str

// File: src/main.rs
// Rust Version: 1.73.0fn main() {let s = String::from("hello world");let hello = &s[0..5]; // [startIndex..endIndex]let world = &s[6..11];println!("'{}' -> '{}' + '{}'", s, hello, world);// Range 语法糖let s_len = s.len();let slice1 = &s[..5];      // 从开头到第5个字节(不含)let slice2 = &s[6..];      // 从第6个字节到结尾let slice_all = &s[..];    // 整个字符串的切片println!("slice1: {}, slice2: {}, slice_all: {}", slice1, slice2, slice_all);
}

输出结果:

'hello world' -> 'hello' + 'world'
slice1: hello, slice2: world, slice_all: hello world

重要提示:字符串切片的索引是基于字节(byte)的。如果你的字符串包含多字节的UTF-8字符,直接对字节索引进行切片可能会导致程序panic,因为它可能恰好切在一个字符的中间。处理UTF-8字符串时,应使用.chars().take()等更安全的方法。

字符串字面量就是切片

我们之前一直在使用的字符串字面量(e.g., "hello"),它的类型其实就是&'static str。它是一个指向存储在程序二进制文件中的、静态生命周期的字符串切片。这也是为什么我们可以把&str类型的变量传递给期望&String的函数(通过解引用强制多态,deref coercions,我们将在后续章节学习)。

4.2 切片与借用规则

字符串切片本质上是不可变引用,因此它们遵循借用规则。

// File: src/main.rs
// Rust Version: 1.73.0fn main() {let mut s = String::from("hello world");let word = first_word(&s);// s.clear(); // 错误!println!("the first word is: {}", word);
}// 这个函数接收一个字符串切片
fn first_word(s: &str) -> &str {let bytes = s.as_bytes();for (i, &item) in bytes.iter().enumerate() {if item == b' ' {return &s[0..i];}}&s[..]
}

在上面的代码中:

  1. first_word(&s)创建了一个对s的切片,这是一个不可变借用。
  2. 如果我们在println!之前尝试调用s.clear()(这是一个需要可变借用的方法),代码将无法编译。
  3. 因为word这个不可变引用在println!中还需要被使用,所以此时不能存在对s的可变借用。

这再次展示了Rust如何通过借用规则来防止迭代器失效等问题。word这个切片依赖于s的内部数据保持不变。如果s.clear()被允许执行,word就会变成一个指向无效内存的悬垂引用。

4.3 其他类型的切片

切片的思想不仅限于字符串。它也适用于其他集合类型,比如数组和向量。

// File: src/main.rs
// Rust Version: 1.73.0fn main() {let a = [1, 2, 3, 4, 5];let slice: &[i32] = &a[1..3]; // slice 的类型是 &[i32]assert_eq!(slice, &[2, 3]);println!("Array slice: {:?}", slice);
}

数组切片&[i32]和字符串切片&str在行为上非常相似。它们都提供了一种安全、高效的方式来共享对集合数据的只读访问,而无需复制数据本身。

总结

在本章中,我们解锁了Rust中除所有权转移之外的另一种核心数据交互方式——借用。

  • 引用(References) (&T&mut T) 允许我们在不获取所有权的情况下访问数据。
  • 借用(Borrowing) 是通过引用使用数据的行为。
  • 借用规则 是编译器强制执行的一套规则,保证了数据安全,核心是在同一作用域内,一个值不能同时被可变地和不可变地借用,也不能同时被多次可变地借用。
  • 悬垂引用 在Rust中是不可能存在的,因为借用检查器会确保所有引用都指向有效的数据。
  • 切片(Slices) (&str, &[T]) 是一种特殊的引用,它指向集合中的一部分连续数据。切片本身不持有所有权,并遵循借用规则,是传递集合只读视图的理想方式。

所有权、借用、引用和切片共同构成了Rust语言安全和性能的基石。初学者可能会觉得借用规则有些苛刻,但请相信,这些规则是你的朋友。它们在编译时为你捕获了整整一个类别的潜在bug,让你在运行时可以高枕无忧。

随着你编写更多的Rust代码,你会逐渐内化这些规则,形成一种“所有权思维”。届时,你将能够自如地在所有权转移和借用之间做出选择,编写出既安全又高效的程序。在接下来的章节中,我们将看到这些概念是如何在更复杂的数据结构(如struct)中应用的。

http://www.dtcms.com/a/557763.html

相关文章:

  • 2025年江西省职业院校技能大赛高职组“区块链技术应用”任务书(6卷)
  • 编译tiff:arm64-linux-static报错 Could NOT find CMath (missing: CMath_pow)
  • SYN关键字辨析,各种锁优缺点分析和面试题讲解
  • 3.1.2.Python基础知识
  • Qt中使用图表库
  • LV.5 文件IO
  • 做目录网站注意沧县网络推广公司
  • 技术准备十五:Elasticsearch
  • 专门做面包和蛋糕的网站山东家居行业网站开发
  • linux挂载新硬盘并提供nfs服务
  • 用asp做宠物网站页面做地方行业门户网站需要什么资格
  • 交易网站建设需要学什么软件电商网站建设济南建网站
  • Python实现从数组B中快速找到数组A中的元素及其索引
  • 高效IT学习指南:用「智能复盘系统」突破学习瓶颈
  • 广东省白云区贵阳seo网站建设
  • 粉色大气妇科医院网站源码彭阳门户网站建设
  • 507-Spring AI Alibaba Graph Human Node 功能完整案例
  • 遥感生态指数(RSEI):理论发展、方法论争与实践进展
  • cjson 的资源释放函数
  • 第6讲:常用基础与布局Widget(一):Container, Row, Column
  • 什么是网站建设塑业东莞网站建设
  • 小企业网站建设哪里做得好深圳网站搭建
  • 婚恋网站策划页面设计好吗
  • 被禁止访问网站怎么办做招聘网站的怎么引流求职者
  • 【架构艺术】自动化测试平台架构设计的一些通用要点
  • 一个做网站的公司年收入宁波最好的推广平台
  • 建设网站0基础需要学什么海口网站建设维护
  • 农产品销售系统|农产品电商|基于SprinBoot+vue的农产品销售系统(源码+数据库+文档)
  • RAG的17种方式实现方式研究
  • 做时间轴的在线网站对网站建设的调研报告