03|所有权:谁负责保存和释放数据
目标:预测赋值与函数调用之后,哪些绑定仍然可以使用,并根据意图选择移动或克隆。
TextLab 将来会把文件读成一个 String。这个类型拥有可增长的 UTF-8 缓冲区;当拥有者被丢弃时,缓冲区通常随之释放。理解所有权的实用问题是:现在谁负责这份数据,以及谁还能访问它?
对于 String,按值赋给另一个绑定或传入函数通常会移动所有权。移动后不能继续用原绑定访问那个值。这里的“移动”描述语言规则,不保证机器执行了一次堆内容复制,也不承诺某种具体优化。整数等实现 Copy 的类型则允许隐式复制,原绑定仍可使用。
只有确实需要两份独立文本时才使用 clone()。它对 String 会复制字符串内容;如果只是想让函数读取数据,下一课的借用更合适。
看一次明确的交接
创建 examples/ownership.rs:
fn consume(text: String) -> usize {
text.len()
}
fn main() {
let original = String::from("rust");
let moved = original;
let backup = moved.clone();
let bytes = consume(moved);
println!("bytes={bytes} backup={backup}");
let count: usize = 4;
let copied = count;
println!("count={count} copied={copied}");
}
String::from 从字面量创建拥有的字符串,len() 返回字节数。运行前先预测:为什么 backup 能在函数返回后使用,而 moved 不能?
cargo run --quiet --example ownership
预期输出:
bytes=4 backup=rust
count=4 copied=4
original 的值先交给 moved,然后 moved 的值交给函数参数 text。consume 读取长度,在函数返回时丢弃参数;返回的是独立的整数。backup 拥有另一份缓冲区,因此仍有效。第二段的整数赋值是复制,两个绑定都有效。
让编译器标出失效的那一步
- 在调用
consume(moved)后面加入println!("{moved}");。 - 运行
cargo check --example ownership,预期失败,诊断包含“borrow of moved value”一类说明,通常标记为 E0382。 - 找到诊断中“发生移动的位置”和“移动后使用的位置”,它们不是同一行。
- 删除新增行,再运行原命令恢复成功状态。
独立练习:实现 add_suffix(mut text: String) -> String,追加 "!" 后把字符串所有权返回。调用者继续使用返回值,整个过程不调用 clone()。
参考实现与原因
完整替换实验文件即可运行:
fn add_suffix(mut text: String) -> String {
text.push_str("!");
text
}
fn main() {
let text = String::from("rust");
let text = add_suffix(text);
assert_eq!(text, "rust!");
println!("{text}");
}
预期输出 rust!。第二个 let text 是遮蔽:建立新绑定接住函数返回的拥有值。mut 允许函数更新字符串,返回值把释放责任移回调用者。
如果你总靠 clone() 让编译通过,先把数据交接画成“调用者 → 参数 → 返回值”,检查是否真的需要第二份数据。如果误以为 mut 能修复移动错误,记住它仅允许修改当前绑定或通过它修改拥有的数据,不会恢复已经移出的值。若调用 len() 得到与你数的汉字不同的结果,第 5 课会解释统计单位。
保留 examples/ownership.rs;主项目还没有接入字符串处理,避免把语言实验与项目功能混在一起。