Rust之所有权与借用详解

Rust 最核心、最独特的特性就是所有权(Ownership);它是 Rust 无需垃圾回收(GC)就能保证内存安全的关键。

所有权

所有权不仅是一组规则,更是一种编译器管理资源的思维模型;其本质是RAII (Resource Acquisition Is Initialization) 的严格实现。

程序运行时会不断申请内存(栈、堆),如何释放则有:

  • 手动管理(C/C++):malloc 后必须记得 free。容易出现:

    • 忘记释放→内存泄漏:可用的系统内存会被逐渐耗尽,最终导致程序运行越来越慢,甚至直接崩溃;
    • 重复释放→双重释放:破坏系统内部的内存管理结构(堆结构),通常会直接触发程序崩溃(Abort),或者造成极其隐蔽的数据损坏和安全漏洞;
    • 指针留而不用→悬垂指针:不小心再次访问或修改它指向的内存,就会引发"未定义行为"------可能是读取到垃圾数据,也可能是直接导致程序段错误(Segmentation Fault)崩溃。
  • 垃圾回收(GC,Java/Go/Python等):语言的运行时(Runtime)或虚拟机(如 JVM)会在后台默默工作,定期启动一个"垃圾回收器",进行"可达性分析":

    • 运行时开销与卡顿(STW):扫描和清理内存时,往往需要暂停所有的业务线程(即著名的 Stop-The-World 现象);
    • 额外的资源消耗:GC 本身运行需要消耗 CPU 和内存资源,且为了保证回收效率,通常会预留一部分内存,内存利用率不如手动管理极致;
    • 无法管理非内存资源:GC 只能回收内存;像文件句柄、网络连接、数据库连接这类"非内存资源",依然需显式关闭。
  • 所有权系统(Rust):每块内存都明确属于某个"所有者", 通过编译器追踪变量的作用域,在编译阶段就确定了内存释放的时机:

    • 零成本抽象:所有内存释放时机都在编译时算好,运行时完全不需要 GC 参与,因此没有 STW 卡顿和额外的运行时内存/CPU 开销,性能直接对标 C/C++。
    • 编译期杜绝内存错误:Rust 的编译器(借用检查器)极其严格;把内存 Bug 的排查从"线上运行时"提前到了"写代码时"。
    • 极高的学习门槛:需要彻底转变思维,去理解"所有权"、"借用(Borrowing)"、"生命周期(Lifetimes)"等概念。

基本规则

Rust 的所有权基本规则------把内存管理的责任在编译阶段就通过严格的规则确定下来:

  • 唯一所有者:Rust 中的每个值(比如一个字符串)在同一时刻只能有一个所有者(Owner)。
  • 作用域决定生死:当所有者离开它的作用域(如函数、代码块结束)时,自动调用 drop 函数释放内存。
  • 所有权转移(Move):把一个值赋给另一个变量,所有权就"移交"了,原来的变量立刻失效。

所有权虽复杂、难掌握,但会带来如下红利:

  • 内存占用可预测:确定性的资源释放

    • 告别 GC 的"随机卡顿";内存回收的时机完全确定:变量一旦离开其作用域,内存就会被立即、自动地释放(调用 drop)。
    • 极致的缓存友好:因为无需 GC扫描,Rust 程序的内存布局通常更加紧凑且连续;极大地提升了 CPU 缓存的命中率,让程序的运行速度更加平稳、可预测。
  • 并发安全:把"数据竞争"扼杀在编译期

    • 编译期拦截并发 Bug:借用检查器在编译阶段强制执行"共享不可变,可变不共享"的铁律;避免了多线程同时修改同一块数据(数据竞争)类的隐蔽 Bug;
    • 无锁编程的可能:编译器排除了 90% 以上的并发冲突,很多场景下甚至不需要加锁就能写出安全的并发代码,极大地降低了并发编程的心智负担。
  • 零成本抽象:跑得快还能写得爽

    • 高级语法,底层速度:编译器会在编译阶段把高级抽象(如 iter().map().filter())直接展开并优化成最底层的机器码;所有的安全检查(如边界检查、所有权规则)也都发生在编译期,最终生成的二进制文件在运行时没有任何额外的"包袱",性能与手写的 C/C++ 代码几乎无异。

drop

drop 机制是确保资源确定性释放(Deterministic Resource Cleanup)的核心:

  • 编译器自动生成析构调用(Drop trait):当一个值离开其所有者作用域时,编译器会自动插入对 drop 方法的调用。
  • 显式所有权终结(std::mem::drop
特性 Drop Trait std::mem::drop
角色 定义者(规定如何清理) 执行者(强制立即清理)
触发机制 编译器自动注入 开发者手动调用
主要目的 实现 RAII,防止内存泄露 管理生命周期,优化资源占用
底层实现 编译器元编程 简单的所有权转移

赋值

Rust 将类型分为两类 :

类型范畴 特征 行为 示例
Copy 类型 数据完全存储在栈上,不持有外部资源。 按位拷贝,赋值后原变量依然有效。 i32, bool, f64, char, 元组 (仅含 Copy 类型)。
Non-Copy 类型 管理堆内存或其他资源。 移动语义,赋值后原变量失效。 String, Vec<T>, Box<T>, 自定义结构体。

赋值时,所有权变化:

类型分类 典型代表 存储位置 赋值行为 原始变量状态
基础标量 i32, bool Copy (按位复制) 保持有效
基础组合 [i32; 10], (f64, bool) Copy 保持有效
智能指针 Box<T>, Rc<T> 栈 + 堆 Move (或引用计数) 失效
动态集合 Vec<T>, String 栈 + 堆 Move 失效
复合结构 struct, enum 取决于成员 默认 Move (除非手动派生 Copy) 失效/部分有效

Move 语义

移动语义(Move Semantics) 是 Rust 内存管理的核心。替代了传统语言中的"深度拷贝"或"引用计数",通过在编译期追踪资源的"所有权"来保证内存安全。

当一个变量赋值给另一个变量或传递给函数时,如果该类型没有实现 Copy Trait,所有权就会发生移动(Move)。

  • 所有权转移:原变量(Source)将资源(堆内存、文件句柄等)的所有权转交给新变量(Target)。
  • 失效状态:一旦所有权移动,原变量进入"未初始化"状态,编译器将禁止再次访问它。
  • 浅拷贝 + 废弃原指针:移动只是简单的栈上的元数据拷贝(如指针、长度、容量),不复制堆数据;关键在于原变量的生命周期被立即终止。

移动出发的场景:

  • 变量赋值
  • 函数参数与返回
  • 闭包捕获: 使用 move 关键字强制闭包获取环境变量的所有权
perl 复制代码
//////////////////////////////////////////
// 函数转移场景
fn take_ownership(s: String) {
    println!("{}", s);
} // s 在此处离开作用域并被释放 (Drop)

let s1 = String::from("Rust");
take_ownership(s1); 
// println!("{}", s1); // 错误!s1 已被函数"吃掉"了

//////////////////////////////////////////
// 闭包转移场景
let data = vec![1, 2, 3];
let handle = std::thread::spawn(move || {
    println!("{:?}", data); // data 所有权移入线程
});

Copy 语义

Copy Trait(隐式的"按位复制"): 告诉编译器,该类型的值可以通过简单的位拷贝(Bitwise Copy/memcpy)来复制,且不会产生任何资源管理问题(原变量依然有效)。

  • 只有在栈上拥有固定大小且不管理外部资源(如堆内存、文件句柄)的类型才能实现 Copy
  • 如果一个类型实现了 Drop Trait(自定义析构逻辑),则不允许实现 Copy

常见类型:所有的标量类型(i32, u64, f64, bool, char)以及只包含 Copy 类型的元组或固定长度数组。

Clone 语义

Clone Trait(显式的"深拷贝"):用于处理需要进行"深度复制"或者涉及复杂逻辑的场景。

需要两个变量同时拥有独立的数据副本时,使用 .clone()

  • 必须通过手动调用 .clone() 来触发。执行开发者定义的逻辑,通常包括在堆上分配新内存并复制数据。
  • 开销可能非常大(取决于数据规模)。
  • Copy 类型必须同时实现 Clone(因为按位拷贝也是一种特殊的克隆),但 Clone 类型不一定要实现 Copy
rust 复制代码
#[derive(Debug, Clone, Copy)] // 允许 Copy
struct Point {
    x: i32,
    y: i32,
}

#[derive(Debug, Clone)] // 仅允许 Clone,因为包含 String(堆数据)
struct User {
    id: u32,
    name: String, 
}

fn main() {
    let p1 = Point { x: 10, y: 20 };
    let p2 = p1; // 自动 Copy,p1 依然可用
    println!("p1: {:?}", p1); 

    let u1 = User { id: 1, name: String::from("Alice") };
    // let u2 = u1;      // 如果这样做,u1 将失效(Move)
    let u2 = u1.clone(); // 显式克隆堆内存,u1 依然有效
    println!("u1: {:?}", u1);
}
特性 Copy Clone
触发方式 隐式(赋值、传参时自动发生) 显式(必须手动调用 .clone()
底层实现 编译器执行 memcpy(极快) 开发者定义逻辑(可能涉及堆分配,慢)
所有权影响 复制值,原值保留 复制值,原值保留
适用场景 简单的栈数据(数值、布尔等) 复杂的资源管理(String, Vec, RC 等)
主要目标 提高便利性,消除 Move 语义的限制 提供一种显式复制复杂数据副本的手段

语义判断

复杂类型的语义取决于"组合法则(Composition)"以及"显式声明(Explicit Opt-in)":

  • 元素 / 标量类型 (Primitives / Scalars) :大小在编译期完全固定,且全部存储在栈(Stack)上 ,且不涉及任何外部资源。

    • 默认Copy (隐式按位复制) : 赋值或传参时,直接执行底层的 memcpy 汇编指令,极度轻量 。
  • 数组 (Arrays [T; N]) 与 元组 (Tuples (T, U)) : 分配在栈上的连续内存块;语义严格绑定于其包含的元素。继承性:

    • 所有元素都实现了 Copy(例如 [i32; 4]),则为Copy 的 ;
    • 只要有一个元素是 Move 语义,则退化为 Move 语义
  • 结构体 (Structs)

    • 默认Move (必须显式 Opt-in 才能 Copy) : 即使结构体的所有字段都分配在栈上且都是 Copy 类型,结构体默认依然是 Move 语义;

    • 让结构体具备 Copy 语义,必须满足两个条件:

      • 所有内部字段都实现了 Copy
      • 显式添加 #[derive(Copy, Clone)] 宏。
  • 集合 (Collections: Vec, String, HashMap 等) : 典型的胖指针(Fat Pointer),栈上存储元数据(指针、长度、容量),堆(Heap)上存储实际变长数据

    • 绝对的 Move 语义,深度克隆需显式 Clone
  • 智能指针 (Smart Pointers) : 系统级编程的核心,它们改变了默认的所有权模型

    • Move 语义,但 Clone 的行为各异

    • Box<T> (独占堆分配)

      • Move: 转移独占所有权。
      • Clone: 触发堆上的深度拷贝(类似集合)。
    • Rc<T> / Arc<T> (引用计数 / 线程安全引用计数)

      • Move: 转移对当前计数的拥有权。
      • Clone: 触发浅拷贝(Shallow Copy);仅增加引用计数,底层堆数据共享。

引用与借用

引用(References) 允许"借用"值而不取得其所有权;本质上是在编译期执行的动态锁策略,旨在彻底消除数据竞争(Data Races)挂起指针(Dangling Pointers)

  • 引用:不拥有所有权的指针,用于临时访问数据。

  • 借用(Borrowing):通过引用访问值的行为,相当于所有权的临时租约;借用检查器(Borrow Checker)会静态分析每一个引用的作用域,确保:

    • 没有悬垂引用
    • 同一时间不存在对同一数据的可变与不可变借用
    • 同一时间最多只有一个可变借用
存在的引用 允许再创建 &T 允许再创建 &mut T
无引用
只有 &T ❌ (不能打破只读)
只有 &mut T ❌ (不能同时存在)

共享借用 &T

共享借用通过不可变引用 &T 实现;可同时存在多个不可变引用,所有人都只能读取,不能修改:

  • 多个 &T 可以共存,因为它们只读,互不干扰:只要不改变数据,多个人同时看是安全的。
  • 在任意一个不可变引用存在的期间,原值不能被移动或修改,但可以通过这些引用读取。
  • 引用作为函数参数,既能让函数访问数据,又不会夺走所有权。

独占借用 &mut T

可变引用 &mut T是独占的;在同一时刻,只能存在一个可变引用;且一旦有了可变引用,就不能再有任何其他引用(无论是可变还是不可变)。

这种"排他性"是为了消除数据竞争(data race):如果一个可变引用在修改数据,同时又有别的引用在读,结果就是未定义行为。Rust 在编译期直接禁止这种可能。

相关推荐
修己xj10 小时前
把上班的每一天,都当成在公司的最后一天
程序员
坚果派·白晓明11 小时前
【鸿蒙PC三方库移植适配框架解读系列】第八篇:扩展lycium框架使其满足rust三方库适配
c语言·开发语言·华为·rust·harmonyos·鸿蒙
毅航13 小时前
AI真能取代程序员?
程序员·ai编程
码力斜杠哥18 小时前
Rust初习录(6)Rust的 if 玩法
开发语言·python·rust
Rust研习社19 小时前
Rust 的 move 语义,一次讲透
后端·rust·编程语言
WMYeah1 天前
【无标题】
前端·rust·抽奖程序·跨平台抽奖程序
浪荡Ddddd1 天前
初识SpringAI:chat篇
后端·程序员
SimonKing1 天前
美团不做外卖做浏览器了,而且是AI浏览器:Tabbit
java·后端·程序员
小兵张健1 天前
30天减20斤挑战:少一斤发100红包(17)
程序员