04|借用:让函数使用数据而不接管它
目标:为只读和修改两种操作设计不同的引用参数,并解释借用冲突。
上课的 consume 接管文本,这对统计函数太强了:统计结束后,调用者还需要显示或继续分析文本。引用让函数临时访问数据,所有权留在原处。
&T 表示共享引用,通过它通常只能读取;&mut T 表示独占的可变引用。在这些普通借用中,同一数据在借用有效期间可以有多个共享引用,或一个可变引用,但不能把相冲突的访问同时保留。这样,当你读某段字符串时,另一个操作就不能通过可变借用改变其缓冲区,使读取建立在失效的位置上。
借用是否还有效与它后面是否被使用有关,不一定延续到大括号结束。这个区别能解释很多“明明在同一作用域,却可以先读再改”的程序。
接口的访问权限写在参数里
创建 examples/borrowing.rs:
fn byte_count(text: &str) -> usize {
text.len()
}
fn append_mark(text: &mut String) {
text.push_str("!");
}
fn main() {
let mut text = String::from("rust");
let view = &text;
println!("before={}", byte_count(view));
append_mark(&mut text);
println!("after={} text={text}", byte_count(&text));
}
这里 &str 是借来的文本视图,下课细讲;&String 在需要 &str 的位置可以自动转换。修改函数使用 &mut String,因为它需要增长字符串,而不仅是看已有字节。
运行前先预测:第一次输出后,view 还会在后面被用到吗?
cargo run --quiet --example borrowing
预期输出:
before=4
after=5 text=rust!
第一次输出是 view 最后一次使用,接着可以独占借用 text。append_mark 返回后,它的可变借用结束,最后又可以共享借用。println! 中的 {} 按顺序接收后面的表达式,也能与 {text} 混用。
延长读取会发生什么
- 在
append_mark(&mut text);后面加上println!("old view={view}");。 - 运行
cargo check --example borrowing,应出现共享借用与可变借用冲突,通常是 E0502。 - 把新增输出移动到
append_mark之前,应恢复编译成功。 - 删除新增输出,恢复主示例。
错误不是说“一个变量不能先读再改”,而是说你安排了在可变访问之后继续使用此前的共享借用。定位引用的最后一次使用,比盲目添加大括号更有帮助。
独立练习:写 append_if_empty(text: &mut String),只在字符串为空时追加 "untitled"。在同一个 main 中验证空串会改变,已有内容不变。is_empty() 返回是否没有内容。
完整参考实现
fn append_if_empty(text: &mut String) {
if text.is_empty() {
text.push_str("untitled");
}
}
fn main() {
let mut empty = String::new();
let mut named = String::from("notes");
append_if_empty(&mut empty);
append_if_empty(&mut named);
assert_eq!(empty, "untitled");
assert_eq!(named, "notes");
println!("{empty} / {named}");
}
替换实验文件后运行同一命令,预期 untitled / notes。String::new() 创建空字符串。函数没有返回新字符串,调用者原有的字符串被修改了。
如果出现“cannot borrow as mutable”,同时检查原绑定的 mut 和调用处的 &mut,两者承担不同角色。如果共享引用与可变引用冲突,按顺序标出创建、修改、最后读取的位置,不要先尝试克隆整个对象。TextLab 的统计接口将接收 &str,因为统计不需要改变输入,也不需要接管输入。
本课新增 examples/borrowing.rs,不修改主入口。