news 2026/4/27 23:18:51

Rust内存安全:所有权与借用 vs 引用计数,该如何选择?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Rust内存安全:所有权与借用 vs 引用计数,该如何选择?

所有权与借用 vs 引用计数

Rust的标志性成就,是在不使用垃圾回收器的情况下实现内存安全。它通过一套严格的所有权系统达成这一目标,但该系统特意设置了一个“逃生出口”:引用计数。

在Rust程序中,每个值在任何给定时刻都只有一个所有者。当该所有者超出作用域时,值就会被丢弃,内存会在执行过程中的已知点被确定性地释放。不会出现垃圾回收暂停、悬空指针或双重释放的问题。编译器会静态地强制执行所有这些规则。

但有些数据确实需要共享,比如图中的一个节点被多条边所拥有、一个配置对象贯穿多个子系统、一个回调持有对周围状态的引用。这时就需要用到引用计数,即 `Rc` 和 `Arc`,它们将所有权逻辑从编译时转移到一个小型的运行时计数器上。

这两者并非相互竞争的特性,而是各有优劣、相互补充的工具。本文将深入剖析这两种机制,包括它们的语义、性能、易用性,并解答一个关键问题:你该如何选择?

所有权与借用

所有权模型是Rust的核心创新。每个堆分配都恰好有一个所有者,即“持有”它的变量绑定。所有权可以转移到另一个绑定,此时原绑定将失效。除非类型实现了 `Copy` 特征,否则它永远不会被隐式复制。

移动语义

代码示例如下:

fn main() {
let s1 = String::from("hello");
let s2 = s1; // 所有权转移 —— s1 现在无效
// println!("{}", s1); ← 编译错误:值在移动后被借用
println!("{}", s2); // 没问题 —— s2 是唯一所有者
} // s2 在此处被丢弃,内存自动释放

借用检查器会强制执行所有权规则。当 `s1` 被赋值给 `s2` 时,编译器会知道 `s1` 不能再被使用,因为它已经放弃了所有权。这在零运行时成本的情况下消除了使用后释放的问题。

借用 —— 共享借用 (`&T`) 和可变借用 (`&mut T`)

在任何地方都转移所有权会很麻烦。Rust允许你借用一个值,即获取一个临时的、有作用域的引用,而不转移所有权。

代码示例如下:

fn calculate_length(s: &String) -> usize {
s.len()
} // s 超出作用域,但不会丢弃 String

fn append_world(s: &mut String) {
s.push_str(", world");
}

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

let len = calculate_length(&greeting); // 共享借用
append_world(&mut greeting); // 可变借用

println!("'{}' has {} characters", greeting, len);
}

借用检查器会同时强制执行两个不变性:

  • 任意数量的共享借用 (`&T`) 可以共存,因为它们是只读的,不会与可变操作产生别名冲突。
  • 同一时间只能有一个可变借用 (`&mut T`),并且不能与任何共享借用共存。

这就是“别名与可变性互斥”原则,它是一条基本规则,能静态地消除一大类错误,如迭代器失效、数据竞争等。

生命周期

引用会附带生命周期注解,用于证明一个引用不会比它所指向的数据存活更久。在大多数代码中,编译器会自动推断生命周期;在复杂的泛型API中,你需要显式地进行注解。

代码示例如下:

// 'a 表示:返回的引用至少和两个输入的生命周期一样长
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() { x } else { y }
}
关键特性

所有权与借用是零成本的,所有的安全保证都在编译时进行验证,不会产生运行时开销,没有计数器递增、除值本身外没有额外的堆分配,也没有额外的间接引用。

引用计数:`Rc` 和 `Arc`

所有权是一种严格的单所有者模型。但有些程序需要共享所有权,即程序的多个部分都需要让同一个值保持存活。经典的例子是图,其中多条边指向同一个节点。

`Rc`(引用计数)会在堆上包裹一个值,并附带一对计数器:强引用计数(活跃的所有者数量)和弱引用计数。每次克隆 `Rc` 时,强引用计数会增加;每次丢弃时,强引用计数会减少。当强引用计数变为零时,内部的值会被丢弃。

基本用法

代码示例如下:

use std::rc::Rc;

fn main() {
let a = Rc::new(String::from("shared data"));
let b = Rc::clone(&a); // 增加强引用计数 —— 廉价的指针克隆
let c = Rc::clone(&a);

println!("strong count = {}", Rc::strong_count(&a)); // 3

drop(b);
println!("after drop b = {}", Rc::strong_count(&a)); // 2
} // a 和 c 在此处被丢弃;计数变为 0,String 被释放
使用 `RefCell` 实现内部可变性

`Rc` 提供了共享所有权,但只能进行不可变访问。要修改内部值,你需要将它与 `RefCell` 结合使用,`RefCell` 会将借用检查从编译时转移到运行时。

代码示例如下:

use std::rc::Rc;
use std::cell::RefCell;

fn main() {
let shared = Rc::new(RefCell::new(vec![1, 2, 3]));

let clone1 = Rc::clone(&shared);
let clone2 = Rc::clone(&shared);

clone1.borrow_mut().push(4); // 运行时借用检查
clone2.borrow_mut().push(5);

println!("{:?}", shared.borrow()); // [1, 2, 3, 4, 5]
}
线程安全的共享:`Arc`

`Rc` 不实现 `Send` 或 `Sync` 特征,因为它的计数器不是原子的,不能跨线程边界使用。对于并发使用,需要使用 `Arc`(原子引用计数),它使用原子CPU操作来实现计数器。要在跨线程场景下实现内部可变性,可以将它与 `Mutex` 或 `RwLock` 结合使用。

代码示例如下:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
let counter = Arc::new(Mutex::new(0u32));

let handles: Vec<_> = (0..8).map(|_| {
let c = Arc::clone(&counter);
thread::spawn(move || {
*c.lock().unwrap() += 1;
})
}).collect();

for h in handles { h.join().unwrap(); }
println!("count = {}", *counter.lock().unwrap()); // 8
}
注意引用循环

`Rc` 和 `Arc` 无法自动检测引用循环。如果两个 `Rc` 值相互持有,它们的强引用计数永远不会变为零,从而导致内存泄漏。可以使用 `Weak` 来创建反向引用(例如树中的父指针),以打破循环。

对比分析

维度所有权与借用`Rc` / `Arc`
所有权模型单所有者,严格执行通过计数句柄实现共享所有权
验证方式编译时 —— 零运行时成本运行时 —— 每次克隆/丢弃时进行计数器操作
性能零开销 —— 无额外间接引用少量开销:堆分配、计数器、指针解引用
线程安全性由 `Send`/`Sync` 编译时规则强制执行`Rc`:仅单线程;`Arc`:线程安全
可变性`&mut T` —— 独占,静态检查需要 `RefCell`/`Mutex` —— 可能会在运行时发生恐慌
循环问题不适用 —— 单所有者不会与自身形成循环引用循环会导致内存泄漏 —— 使用 `Weak` 解决
API复杂度学习曲线较陡(涉及生命周期)表面上更简单,复杂度转移到运行时
典型用例Rust中几乎所有数据的默认选择具有共享节点的图、树,共享配置,回调
丢弃时机确定性 —— 在所有者作用域结束时确定性 —— 最后一个句柄丢弃时(可能不明显)
克隆成本深拷贝(或移动 —— 免费)指针复制 + 计数器递增 —— O(1)
实际性能表现

`Rc` 的开销主要体现在三个方面:为控制块额外进行一次堆分配、每次访问时进行一次指针间接引用,以及在克隆和丢弃时进行两次整数递增/递减操作。对于大多数程序来说,这些开销可以忽略不计。但对于每秒执行数百万次操作的热点代码,如游戏引擎、编译器、信号处理器等,优先使用拥有所有权的值更为重要。

`Arc` 会增加额外的成本,因为原子操作使用了CPU内存顺序保证(`SeqCst` 或 `Release`/`Acquire`),这会抑制某些编译器和硬件的重排序。在竞争激烈的多核工作负载中,这种开销可能会变得很明显。

经验法则

当使用复杂的生命周期注解或不安全代码来绕过借用检查器时,可以考虑使用 `Rc`/`Arc`。虽然会有少量的运行时成本,但能为你带来易用且安全的共享所有权,这在大多数应用程序代码中是一个合理的权衡。

决策指南

优先选择所有权 + 借用的情况
  • 数据有明确的单一逻辑所有者(大多数结构体、集合、I/O资源)。
  • 需要确定性的、低开销的资源销毁(如文件、套接字、锁的RAII机制)。
  • 编写库代码,希望调用者能够控制生命周期。
  • 性能至关重要,每个分配都很关键。
优先选择 `Rc` / `Arc` 的情况
  • 多个所有者确实需要让同一数据保持存活(如图节点、解析树、事件监听器)。
  • 数据的生命周期在运行时动态确定,而非静态确定。
  • 需要与期望引用语义的外部系统进行交互(如Python扩展、GUI框架)。
  • 希望在不复制大型结构的情况下实现跨线程的共享状态(`Arc>`)。

代码示例如下:

// 典型用例:具有共享节点的图
use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
value: i32,
children: Vec<Rc<RefCell<Node>>>,
parent: Option<Weak<RefCell<Node>>>, // Weak 打破父节点 -> 子节点的循环
}

impl Node {
fn new(value: i32) -> Rc<RefCell<Node>> {
Rc::new(RefCell::new(Node {
value,
children: vec![],
parent: None,
}))
}
}
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/27 23:18:23

构建个人技能库:从脚本到架构的工程化知识管理实践

1. 项目概述&#xff1a;一个技能库的诞生与价值在技术社区里&#xff0c;我们经常能看到一些以个人或组织命名的代码仓库&#xff0c;比如fioenix/huly-skill。乍一看&#xff0c;这个名字可能有些抽象&#xff0c;它不像一个具体的工具或框架那样直白。但恰恰是这种命名方式&…

作者头像 李华
网站建设 2026/4/27 23:10:28

Caveman开源项目:用提示词工程优化AI对话,节省75%的Token成本

1. 项目概述&#xff1a;当AI学会“说人话”&#xff0c;我们到底在省什么&#xff1f;如果你和我一样&#xff0c;每天要和Claude、GPT这类大模型对话几十上百次&#xff0c;那你肯定对一种现象深恶痛绝&#xff1a;AI的“废话文学”。明明一句话就能说清楚的事&#xff0c;它…

作者头像 李华
网站建设 2026/4/27 23:05:55

基于MCP协议的智能代码审查助手:从原理到实践

1. 项目概述&#xff1a;一个为代码审查注入“灵魂”的智能助手 如果你是一名开发者&#xff0c;或者参与过任何规模的软件项目&#xff0c;那么“代码审查”这个词对你来说一定不陌生。它可能是团队协作中最有价值、也最令人头疼的环节之一。有价值在于&#xff0c;它能提前发…

作者头像 李华
网站建设 2026/4/27 23:05:37

手把手教你用R和fgsea包,从差异基因列表到发表级GSEA图的保姆级教程

从差异基因到发表级GSEA图&#xff1a;R语言全流程实战指南 在转录组数据分析领域&#xff0c;基因集富集分析(GSEA)已成为揭示生物学意义的重要工具。与传统的差异基因分析不同&#xff0c;GSEA能够发现那些虽然单个基因变化不大但整体协调变化的通路&#xff0c;这对于理解复…

作者头像 李华
网站建设 2026/4/27 23:02:42

HoRain云--PowerShell Cmdlet高效管理指南

&#x1f3ac; HoRain 云小助手&#xff1a;个人主页 ⛺️生活的理想&#xff0c;就是为了理想的生活! ⛳️ 推荐 前些天发现了一个超棒的服务器购买网站&#xff0c;性价比超高&#xff0c;大内存超划算&#xff01;忍不住分享一下给大家。点击跳转到网站。 目录 ⛳️ 推荐 …

作者头像 李华
网站建设 2026/4/27 22:57:38

文心一言和DeepSeek V4哪个更好?

做长文本 / 代码 / 深度推理选 DeepSeek V4&#xff1b;做中文合规 / 多模态 / 搜索联动选文心一言 5.0。下面从核心差异、能力对比、场景选型三方面说清楚。一、核心差异&#xff08;一眼看懂&#xff09;表格对比项文心一言 5.0&#xff08;ERNIE 5.0&#xff09;DeepSeek V4…

作者头像 李华