所有权模型

本页目录

第一次读到哪里: 先看 move、Copy/Clone 与 Drop 三节各自开头的最小例子,完成“谁还能使用这个值、谁负责释放”的判断,再去引用与借用。文中的共享指针、闭包捕获、部分移动和复杂析构顺序留作第二遍查阅;不用在第一天掌握所有边界。

阅读准备⁠:会写变量与函数,能运行一个 Cargo 示例。⁠学完应能⁠:解释一次 move 后谁拥有资源,区分 Copy、Clone 与 Drop。第一次学 Rust 可先看 入门与学习路线。

所有权回答“谁负责这个值、何时可以交出去、最后如何清理”。Rust 结合移动、借用检查和类型封装,静态限制重复释放与悬垂访问;资源本身的申请、引用计数或析构仍可能有运行时成本。

初学时可以先看一个拥有资源的 String:正常路径上,它的资源责任随所有者移动,最终由仍有效的所有者清理。Rc/Arc 允许多个句柄共享资源,各个句柄本身又受普通所有权规则管理,所以“任何资源在所有层面永远只有一个拥有者”并不准确。

move 转移可用性与资源责任

移动前:s1 拥有 String负责最终释放缓冲同一份字节缓冲hello移动后:s2 拥有 Strings1 不能再作为已初始化值使用语义示意;不规定变量必须在栈上,也不要求机器码实际复制固定数量的字。移动转交资源责任,不复制字符串缓冲 move 转移资源责任 move 转移资源责任 移动前:s1 拥有 String 由 s1 负责最终清理 move 转交拥有权 移动后:s2 拥有 String s1 不再是已初始化的值 s2 指向原缓冲 仍是同一份字节缓冲 hello;没有克隆字符串 这是语义示意,不规定变量位于栈上,也不要求机器码复制固定数量的字。
fn consume(text: String) -> usize { text.len() }
fn main() {
    let first = String::from("hello");
    let second = first;
    assert_eq!(consume(second), 5);
    // first 已移动;second 又交给 consume,均不能再读取。
}

赋值、按值传参、返回值和按值解构都可能移动。移动后原位置不能继续当作已初始化的值使用,但可在允许的情况下重新赋值。编译器不需要每次在运行时写一个“失效标记”;具体实现可以消除搬运,某些控制流则需要追踪是否析构。

String 的缓冲不会因为简单移动而被克隆。这个结论来自类型与操作的语义,不能简化为“栈上的位复制都安全”:栈上也可能放着不可重复拥有的资源句柄。

接收所有权的接口,也可以在失败时退回资源

fn admit(text: String, max_bytes: usize) -> Result<String, String> {
    if text.len() <= max_bytes { Ok(text) } else { Err(text) }
}
fn main() {
    let input = String::from("message");
    let mut returned = admit(input, 3).unwrap_err();
    assert_eq!(returned, "message");
    returned.truncate(3);
    assert_eq!(admit(returned, 3).unwrap(), "mes");
}

第一次调用后 input 已移动,但返回的 Err 又把同一字符串的拥有权交还调用者。失败不意味着值必须被丢弃,也不意味着原变量自动重新可用;资源是否能重试,取决于错误类型有没有保留它。真实接口可以用 Rejected { input, reason } 同时返回输入和错误原因。

参数或返回形式调用者交出什么常见用途
fn inspect(value: &T)临时共享访问读取内容,继续保留原值
fn update(value: &mut T)临时独占访问在原位置修改合法状态
fn consume(value: T)值的拥有权保存、转交或最终清理
Result<T, (T, E)>成功或失败分支都归还一个 T需要保留可重试资源

先按这些行为设计接口,再决定是否需要 clone。为了“调用以后还能用”一律 clone,可能掩盖本来只需要借用的 API,也可能意外创建共享状态而非独立快照。

Copy 是隐式复制能力,Clone 是显式操作

操作原变量是否还能用可以执行自定义逻辑吗
非 Copy 值的 move通常不能继续读取已移动部分不调用 Clone
Copy 类型的赋值可以Copy 本身不执行自定义复制方法
value.clone()通常可以由 Clone 实现决定复制与共享行为

用户结构体不会仅因字段都 Copy 就自动实现 Copy;需要显式实现或 derive,并满足字段约束。Copy 要求 Clone,不能用于实现了 Drop 的类型。共享引用 &T 是 Copy,可变引用 &mut T 不是,避免随意复制独占访问权。Copy

Clone 不等于深拷贝。String::clone 通常创建独立缓冲;Arc::clone 创建指向同一资源的新所有者。派生 Clone 会调用字段的 Clone,因此里面的 Arc 仍然共享。Clone

use std::rc::Rc;
fn main() {
    let original = String::from("a");
    let mut independent = original.clone();
    independent.push('b');
    assert_eq!(original, "a");
    let shared = Rc::new(original);
    let another = Rc::clone(&shared);
    assert!(Rc::ptr_eq(&shared, &another));
}

移动的是责任,克隆的是哪一层?

把“变量还能不能用”和“资源是不是同一份”放在两层看。比较三种操作:move 改变负责资源的绑定,String::clone 增加独立文本,Rc::clone 增加共享拥有者。图中的箭头表示拥有关系,不表示借用,也不承诺具体机器码。

正在呈现知识画面
移动的是责任,克隆的是哪一层?

移动使旧绑定失效;String 的克隆创建独立缓冲,Rc 的克隆共享缓冲。

闭包捕获按使用方式决定

非 Copy 不意味着必然被闭包移动。只读 Vec 的闭包通常可以共享借用;需要修改时可能独占借用,消费值时才需要取得所有权。move 关键字要求按值捕获,但移动引用也只转移引用本身,不延长被引用对象生命期。闭包捕获

fn main() {
    let values = vec![1, 2, 3];
    let read = || values.len();
    assert_eq!(values[0], 1); // 闭包只读借用,外部也能读
    assert_eq!(read(), 3);
    let owned = move || values.len();
    assert_eq!(owned(), 3);
    assert_eq!(owned(), 3); // move 捕获也不等于只能调用一次
}

闭包能实现 Fn、FnMut 或 FnOnce 取决于调用时怎样使用捕获值,和是否写 move 是不同问题。

部分移动与重新建立完整值

fn main() {
    let mut pair = (String::from("left"), String::from("right"));
    let left = pair.0;
    assert_eq!(pair.1, "right"); // 未移动字段仍可访问
    pair.0 = String::from("new");
    assert_eq!(pair, (String::from("new"), String::from("right")));
    assert_eq!(left, "left");
}

部分字段移动后,不能把整体当作完整值使用;补回该字段后可以恢复。对于自身实现 Drop 的结构,普通代码不能随意移出非 Copy 字段,因为其析构方法仍需要有效的 self。常见做法是用 mem::replace、mem::take 或 Option::take 留下合法替代值。

从实现 Drop 的对象中取出字段,要留下可析构状态

use std::{cell::Cell, rc::Rc};
struct Package {
    payload: Option<String>,
    released: Rc<Cell<usize>>,
}
impl Drop for Package {
    fn drop(&mut self) {
        self.released.set(self.released.get() + 1);
    }
}
fn main() {
    let released = Rc::new(Cell::new(0));
    let mut package = Package {
        payload: Some(String::from("data")),
        released: Rc::clone(&released),
    };
    let text = package.payload.take().unwrap();
    assert!(package.payload.is_none());
    drop(package);
    assert_eq!(released.get(), 1);
    assert_eq!(text, "data");
}

Option::take 把字段替换成 None,再将原来的 Some 内容交出去;Package 的 Drop 仍面对一个完整值,移出的 String 则独立存活。若业务不允许空状态,应该用其他合法替代值,或设计消费整个对象的方法,而不是为了绕过错误把不变量改成任意可空。

mem::take 使用 Default 留下替代值,mem::replace 使用指定的新值。它们转移旧值,不会因为“取出”就立即析构旧值;新旧资源接下来分别由各自拥有者管理。mem::take

Drop 顺序与边界

普通局部变量在退出其析构作用域时按声明逆序释放;结构体字段则按声明顺序释放,且先执行结构自身的 Drop 方法。数组元素从前向后释放。临时值、模式与控制流有更细的作用域规则,不能把一个“逆序”套给所有对象。析构规则

显式 drop(value) 是把值交给一个按值参数,从而提前释放;drop(&value) 只处理一个引用,不释放原资源。借用提前结束,也不等于拥有者的 Drop 提前执行。

局部声明:earlier → group作用域结束先处理 groupgroup 字段:first → second先 group.drop,再 first,再 second完整事件序列group → first → second → earlier局部变量的逆序与字段的正序可以同时出现本例没有移动字段或提前 drop;其他控制流应按仍已初始化的值逐个分析。 Drop 顺序分两层看 Drop 顺序分两层看 局部声明:earlier → group 退出作用域时先处理 group 局部变量按逆序 group 字段:first → second 先执行 group.drop,再析构字段 字段按声明顺序 完整事件序列 group → first → second → earlier 本例没有移动字段或提前 drop。其他控制流要检查各值是否仍已初始化。

用析构事件验证顺序,而不是猜堆栈布局

use std::{cell::RefCell, rc::Rc};
type Log = Rc<RefCell<Vec<&'static str>>>;
struct Item(&'static str, Log);
impl Drop for Item {
    fn drop(&mut self) { self.1.borrow_mut().push(self.0); }
}
struct Group { first: Item, second: Item, log: Log }
impl Drop for Group {
    fn drop(&mut self) {
        assert_eq!(self.first.0, "first");
        assert_eq!(self.second.0, "second");
        self.log.borrow_mut().push("group");
    }
}
fn main() {
    let log = Rc::new(RefCell::new(Vec::new()));
    {
        let _earlier = Item("earlier", Rc::clone(&log));
        let _group = Group {
            first: Item("first", Rc::clone(&log)),
            second: Item("second", Rc::clone(&log)),
            log: Rc::clone(&log),
        };
    }
    assert_eq!(*log.borrow(), ["group", "first", "second", "earlier"]);
}

_group 是一个具名绑定,前面的下划线只让未使用绑定不发警告,仍会持有值直到作用域结束。它与通配模式 _ 不同:let _ = 构造资源(); 不建立这样的拥有者绑定,临时资源通常在该语句结束时释放;锁 guard 尤其不应靠这种写法保留。

Group::drop 运行时字段还有效,方法结束后 Rust 再递归析构字段,无需手工重复释放 first/second。这里用 RefCell 记录事件便于观察;真实 Drop 通常应避免新增可能失败的工作,特别是展开过程中再次 panic 的路径。析构规则

正常退出和 panic 展开会遵循适用的析构规则,但 mem::forget、引用循环、进程终止或 abort 等可能让清理不发生。Rust 允许内存泄漏,因此安全抽象不能依赖析构必定执行才能避免 UB。所有权提供的重点是安全的访问与责任转移,而不是承诺所有资源在所有终止路径都被回收。

继续阅读 引用与借用,理解如何使用值而不取得其所有权。