Agent X-Ray
RuntimeNotesAbout
Notes/代码工程/Rust 深度教材/第3章

第3章:所有权 —— 借用、切片与内存模型

9 分钟 · 更新于 2026-09-01

第3章:所有权 —— 借用、切片与内存模型

本章把所有权当作一套“资源账本”来学习:创建值意味着登记负责人,move 意味着转交责任,借用意味着临时访问,作用域结束意味着结算。沿着这条主线,再理解 CopyClone、切片和字符串会更自然。


一、先建立资源账本

每一份需要清理的资源都必须回答两个问题:谁负责释放,以及其他代码如何临时使用它。手动内存管理把答案交给程序员,垃圾回收把答案延迟到运行时;Rust 则让编译器在编译阶段核对一份资源账本。

可以用四个动作理解本章:

动作账本变化
创建拥有型值登记一个所有者
move把清理责任转给新所有者
借用临时授予读或写权限,不改变所有者
离开作用域所有者执行清理

这套模型覆盖的不只是堆内存。文件句柄、锁守卫、网络连接等资源同样可以在所有者离开作用域时确定释放。所有权因此既是内存规则,也是通用的资源管理协议。

遇到借用错误先画关系 写出所有者、引用创建点、最后使用点和 move 点。很多看似复杂的生命周期问题,画成时间线后只是“权限重叠”或“资源已转交”。

二、栈、堆与 String

栈上的值通常大小已知。函数调用时参数和局部变量进入栈帧,函数返回时栈帧退出,分配速度很快。

rust
let x: i32 = 42;
let enabled: bool = true;
let point: (i32, i32) = (3, 4);

运行时大小可能变化的数据通常需要堆分配,例如:

rust
let mut message = String::from("hello");
message.push_str(", Rust");

可把 String 简化理解为栈上的三个字段:指向堆数据的指针、当前长度、已分配容量。真正的 UTF-8 字节存放在堆上,栈上的描述信息大小固定。

栈与堆只是入门模型 Rust 不承诺一个类型永远位于某个区域。值可能被优化、嵌入其他结构或装箱。本章用常见布局建立直觉,不把它当作绝对位置保证。

三、所有权三条规则

  1. 每个值都有一个所有者
  2. 同一时刻,一个值通常只有一个所有者
  3. 所有者离开作用域时,值被丢弃并执行清理
rust
fn main() {
    {
        let text = String::from("scope");
        println!("{text}");
    } // text 被 drop
}

资源释放发生在确定的作用域边界,因此时机可预测。

四、Copy:简单值按位复制

rust
fn main() {
    let x = 5;
    let y = x;

    println!("x={x}, y={y}");
}

i32 实现了 Copy,赋值时复制值,两个绑定都有效。常见 Copy 类型包括整数、浮点数、boolchar、元素全部为 Copy 的元组与数组,以及不可变引用 &T

String 的元组通常不是 Copy。是否 Copy 由 trait 实现决定,不是简单由“看起来在栈上”决定。实现了资源清理语义的类型通常不能 Copy,否则多个副本可能清理同一资源。

五、move:转移清理责任

rust
fn main() {
    let first = String::from("hello");
    let second = first;

    println!("{second}");
    // println!("{first}"); // first 已移动
}

赋值时,指针、长度和容量被复制到 second,堆上的文本没有深拷贝。为避免两个变量二次释放同一内存,Rust 让 first 失效,这叫 move

move 首先是语义规则:清理资源的责任从旧绑定转移给新绑定,不必理解成真的把所有字节搬了位置。

遇到 borrow of moved value,先问:旧变量是否还需要;函数是否只需借用;是否真的要独立副本;能否让最后一次使用发生在 move 之前。

六、Clone:显式创建副本

确实需要两份独立堆数据时使用 clone()

rust
fn main() {
    let first = String::from("hello");
    let second = first.clone();

    println!("first={first}, second={second}");
}

Clone 可能包含堆分配和逐元素复制,因此必须显式调用。

行为是否显式原值可用可能复制堆数据
Copy通常否
move
clone()可能

七、函数参数也会 Copy 或 move

rust
fn consume(text: String) {
    println!("{text}");
}

fn inspect(number: i32) {
    println!("{number}");
}

fn main() {
    let text = String::from("owned");
    consume(text);
    // println!("{text}"); // 已移动

    let number = 42;
    inspect(number);
    println!("{number}"); // i32 是 Copy
}

函数也能返回所有权:

rust
fn add_suffix(mut text: String) -> String {
    text.push('!');
    text
}

fn main() {
    let text = String::from("hello");
    let text = add_suffix(text);
    println!("{text}");
}

如果函数只想读取,来回传递所有权会很啰嗦,借用正是为此而生。

八、借用:使用值,不接管清理责任

rust
fn length(text: &String) -> usize {
    text.len()
}

fn main() {
    let text = String::from("hello");
    let size = length(&text);

    println!("{text} 的长度是 {size}");
}

&text 创建引用,函数只借用,不取得所有权,所以函数结束后 text 仍归调用者。

只读文本参数通常更适合写成 &str

rust
fn length(text: &str) -> usize {
    text.len()
}

fn main() {
    let owned = String::from("hello");
    let literal = "world";

    println!("{}", length(&owned));
    println!("{}", length(literal));
}

&str 既能接收字面量,也能接收 String 的视图,因此比 &String 更通用。

九、引用、解引用与共享读取

& 创建引用,* 访问引用指向的值:

rust
let x = 5;
let reference = &x;
assert_eq!(x, 5);
assert_eq!(*reference, 5);

xi32reference&i32,二者类型不同。方法调用常有自动解引用,但理解 * 对阅读错误和智能指针代码很重要。

同一时刻可以有多个不可变引用:

rust
let text = String::from("hello");
let first = &text;
let second = &text;
println!("{first}, {second}");

多个读者不会互相破坏数据。通过 &String&str 不能修改原值。

十、可变借用:独占写入

修改被借用的值,需要原绑定可变,并创建 &mut

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

fn main() {
    let mut text = String::from("hello");
    append_world(&mut text);
    println!("{text}");
}

关键限制是:同一时刻,对同一数据只能存在一个有效的可变引用。

rust
let mut text = String::from("hello");
let first = &mut text;
let second = &mut text; // 编译错误
println!("{first}, {second}");

这条规则在编译期排除数据竞争的重要前提:多个访问者同时访问同一数据、至少一个写入、又没有同步机制。

十一、共享读与独占写不能重叠

rust
let mut text = String::from("hello");
let reader = &text;
let writer = &mut text; // reader 后面仍使用,因此错误
println!("{reader}, {writer}");

读者假设读取期间数据不会被别人修改。但借用不一定持续到花括号末尾。非词法生命周期(NLL)通常让引用在最后一次使用后结束:

rust
let mut text = String::from("hello");

let first = &text;
let second = &text;
println!("{first}, {second}");

let writer = &mut text;
writer.push('!');
println!("{writer}");

共享借用已经在 println! 后结束,因此后面可以独占写入。

借用规则一句话 同一时刻,要么任意多个共享读者,要么一个独占写者;所有引用都必须始终有效。

十二、悬垂引用为什么被拒绝

rust
fn dangle() -> &String {
    let text = String::from("hello");
    &text
}

函数结束时 text 被释放,返回引用会指向无效内存,因此编译器拒绝。正确做法是返回所有权:

rust
fn no_dangle() -> String {
    let text = String::from("hello");
    text
}

返回时所有权移动给调用者。错误信息可能提到生命周期;当前先理解为引用不能比被引用值活得更久。

十三、切片:借用一段连续数据

数组切片:

rust
fn main() {
    let numbers = [10, 20, 30, 40, 50];
    let middle = &numbers[1..4];

    assert_eq!(middle, &[20, 30, 40]);
}

middle 类型是 &[i32],保存起始地址和长度,不拥有元素。

字符串切片:

rust
fn main() {
    let text = String::from("hello world");
    let hello = &text[0..5];
    let world = &text[6..11];

    println!("{hello}, {world}");
}

范围左闭右开。还可以写 &text[..5]&text[6..]&text[..]。字符串切片类型是 &str

十四、切片为什么总以引用出现

str[T] 是动态大小类型(DST),仅看类型无法知道具体长度,因此不能像普通局部值那样直接使用:

rust
// let text: str = "hello"; // 大小未知

实际使用的是:

  • &str:字符串切片引用
  • &[T]:序列切片引用

切片引用通常是“宽指针”,包含数据起始地址和长度,这两个字段大小固定。

底层切片常用形式含义
str&str有效 UTF-8 字节的借用视图
[i32]&[i32]连续 i32 元素的借用视图

日常说“字符串切片”通常指 &str,说“数组切片”通常指 &[T]

十五、Stringstr&str&String

String 标准库拥有型字符串,拥有可增长的堆上 UTF-8 字节,move 时转移所有权:

rust
let mut text = String::from("Rust");
text.push('!');

str 语言内建的动态大小字符串切片类型,几乎不会单独作为局部变量,通常通过 &strBox<str> 等指针形式使用。

&str 对 UTF-8 字符串数据的只读借用,不拥有字符数据,可以来自字面量或 String

rust
let literal: &str = "hello";
let owned = String::from("world");
let view: &str = &owned;

&StringString 对象本身的借用。只读文本 API 通常优先接收 &str,因为调用者既可传字面量,也可传 String 的视图。

十六、UTF-8:字符串不能按字符下标访问

Rust 字符串是 UTF-8 字节序列。英文字符通常占 1 字节,常见汉字通常占 3 字节。下面代码无效:

rust
let text = String::from("中国");
// let first = text[0];

如果返回字节,它不一定是完整字符;如果返回第 N 个字符,定位过程又无法保证 O(1)。Rust 因而不提供整数下标访问 String

按 Unicode 标量值遍历:

rust
for character in "中国人".chars() {
    println!("{character}");
}

按底层字节遍历:

rust
for byte in "中国人".bytes() {
    println!("{byte}");
}

字符串切片必须落在字符边界:

rust
let text = "中国人";
let first = &text[0..3]; // 合法
// let broken = &text[0..2]; // 运行时 panic

len() 返回字节数 "中国人".len() 是 9,不是 3。.chars().count() 返回 Unicode 标量值数量,但它也不一定等于用户眼中的字形数量。

十七、切片让 API 与所有权同步

实现“第一个单词”:

rust
fn first_word(text: &str) -> &str {
    for (index, byte) in text.bytes().enumerate() {
        if byte == b' ' {
            return &text[..index];
        }
    }

    text
}

fn main() {
    let sentence = String::from("hello Rust");
    let word = first_word(&sentence);
    println!("{word}");
}

返回值是原文本的一段 &str,没有分配和复制。如果切片仍会使用,就不能修改原字符串:

rust
let mut sentence = String::from("hello Rust");
let word = first_word(&sentence);
// sentence.clear(); // word 仍借用 sentence
println!("{word}");

先完成切片的最后一次使用,再修改则可以:

rust
let mut sentence = String::from("hello Rust");
let word = first_word(&sentence);
println!("{word}");

sentence.clear();

切片不仅保存开始位置和长度,还让编译器知道视图依赖原数据,从而阻止原数据在视图有效期间被破坏。

十八、怎样读借用检查错误

按顺序排查:

  1. 确定类型:String&str&String 还是其他类型
  2. 标出所有者:谁最终负责 drop
  3. 找到 move 点:赋值、传参或方法是否接收 self
  4. 找到借用开始:哪里创建 &T&mut T
  5. 找到最后一次使用:NLL 通常在这里结束借用
  6. 判断真实意图:转移、只读、修改还是独立副本
  7. 选择最小修复:调整顺序、缩短借用、改参数为引用,最后才考虑 clone

例如:

rust
fn print_length(text: String) {
    println!("{}", text.len());
}

let text = String::from("hello");
print_length(text);
// println!("{text}"); // 已移动

函数只读取长度,更合理的接口是:

rust
fn print_length(text: &str) {
    println!("{}", text.len());
}

let text = String::from("hello");
print_length(&text);
println!("{text}");

不要先写 print_length(text.clone())。那虽然能通过,却掩盖了函数根本不需要所有权。

十九、常见误区

  1. move 就是深拷贝:move 通常只转移清理责任,不复制堆数据
  2. 所有栈上值都 Copy:是否 Copy 由 trait 实现决定
  3. 所有权错误就加 clone:先判断函数是否应该借用
  4. &String&str 没区别:只读文本参数通常优先 &str
  5. 字符串下标按字符计数:字符串切片使用字节偏移,并要求 UTF-8 边界
  6. &mut T 只是可修改指针:它还代表暂时的独占访问权
  7. 引用活到花括号末尾:现代 Rust 通常在最后一次使用后结束借用
  8. 切片拥有原数据:切片只是依赖原数据的借用视图

二十、实践练习

练习一:标注资源流向

阅读下面的代码,在每行旁标出 create、borrow、move 或 drop,并预测最后一行能否编译:

rust
let report = String::from("weekly");
let view = &report;
println!("{view}");
let archived = report;
println!("{archived}");

随后把 println!("{view}") 移到 move 之后,解释错误发生的原因。

练习二:缩短可变借用

编写函数 normalize_title(&mut String):去掉首尾空白并转成小写。尝试同时保留一个 &str 视图并修改原字符串,再通过缩短引用的最后使用位置修复冲突。

练习三:返回切片而非索引

实现 first_token(input: &str) -> &str,返回第一个空白字符之前的切片。验证空字符串、无空白、多字节中文和连续空白输入。

练习四:设计参数类型

分别为“只读取文本”“需要追加内容”“需要保存并稍后使用”设计函数签名。比较 &str&mut StringString 各自表达的所有权承诺。

练习五:控制复制成本

创建包含 String 的结构体,分别尝试赋值、借用和 clone()。记录每种写法是否让原变量继续可用,以及是否可能复制堆数据。

二十一、总结

  • 所有权是一份编译期资源账本:每个资源有负责人,负责人离开作用域时执行清理。
  • Copy 让低成本值隐式复制;move 转交清理责任;Clone 显式创建独立副本。
  • 借用不会改变所有者,&T 提供共享读取,&mut T 提供独占写入。
  • 安全借用的核心规则是“多个读者或一个写者”,并且引用不能超过原值的有效范围。
  • NLL 通常让借用在最后一次使用后结束,因此分析错误时应关注真实使用区间。
  • 切片是带长度的借用视图;String 拥有 UTF-8 数据,&str 只观察其中一段。
  • API 设计应优先表达真实需求:能借用就不接管,能接收 &str 就不把调用者限制为 String

下一章开始把资源规则用于领域建模:结构体表达“同时存在”,枚举表达“只能选其一”,模式匹配负责完整处理每一种状态。

请继续阅读:第4章:数据建模 —— 结构体、枚举、模式匹配与方法。


  • 第1章:开篇 —— Rust 为什么值得学与环境搭建
  • 第2章:语言基础 —— 变量、类型、函数与流程控制
  • 第4章:数据建模 —— 结构体、枚举、模式匹配与方法

本章目录
一、先建立资源账本二、栈、堆与 String三、所有权三条规则四、Copy:简单值按位复制五、move:转移清理责任六、Clone:显式创建副本七、函数参数也会 Copy 或 move八、借用:使用值,不接管清理责任九、引用、解引用与共享读取十、可变借用:独占写入十一、共享读与独占写不能重叠十二、悬垂引用为什么被拒绝十三、切片:借用一段连续数据十四、切片为什么总以引用出现十五、String、str、&str 与 &String十六、UTF-8:字符串不能按字符下标访问十七、切片让 API 与所有权同步十八、怎样读借用检查错误十九、常见误区二十、实践练习二十一、总结Related Documents
苏ICP备2025204887号-2