《Rust编程实战》系列第35篇
在上一篇文章中,我们学习了Rust的Copy,了解了哪些轻量数据类型可以在赋值或传参时自动复制。本篇继续进入Rust所有权体系中非常重要的概念:
1Borrow 2
中文通常称为“借用”。
前面我们已经知道,String、Vec等非Copy类型传递给另一个变量或函数时,通常会发生所有权移动:
1fn print_name(name: String) { 2 println!("{}", name); 3} 4 5fn main() { 6 let name = String::from("Tom"); 7 print_name(name); 8} 9
调用print_name(name)后,name的所有权进入函数,后面不能继续使用:
1println!("{}", name); 2
如果函数只是想读取字符串,并不需要真正拥有它,那么直接转移所有权显得非常浪费。
Rust为此提供了借用机制:
1&T 2
例如:
1fn print_name(name: &String) { 2 println!("{}", name); 3} 4 5fn main() { 6 let name = String::from("Tom"); 7 print_name(&name); 8 println!("{}", name); 9} 10
这次函数只是临时借用了name,所有权仍然属于main中的变量。
本文将介绍:
- 什么是Borrow
- 为什么需要借用
- 引用
&T - 借用与Move的区别
- 多个不可变引用
- 函数参数中的借用
&String与&str- Vec和切片借用
- 结构体借用
- 解引用
* - 借用作用域
- Borrow Checker
- 常见借用错误
- Borrow最佳实践
什么是Borrow
Borrow表示:
临时访问一个值,但不取得这个值的所有权。
例如:
1fn main() { 2 let text = String::from("Rust"); 3 let reference = &text; 4 println!("{}", reference); 5 println!("{}", text); 6} 7
这里:
1&text 2
创建了一个指向text的引用。
变量:
1reference 2
只是借用了text,并没有拥有字符串数据。
因此:
1text 2
仍然可以继续使用。
什么是引用
Rust中使用:
1&T 2
表示某个类型T的不可变引用。
例如:
1&i32 2&String 3&Vec<i32> 4&User 5
如果:
1let number = 100; 2
那么:
1let reference = &number; 2
reference的类型就是:
1&i32 2
示例:
1fn main() { 2 let number = 100; 3 let reference: &i32 = &number; 4 println!("{}", reference); 5} 6
引用可以理解为:
一个指向原数据的安全地址,但它受到Rust借用规则约束。
为什么需要借用
假设有一个函数用于计算字符串长度:
1fn get_length(text: String) -> usize { 2 text.len() 3} 4
调用:
1fn main() { 2 let text = String::from("Hello Rust"); 3 let length = get_length(text); 4 println!("长度:{}", length); 5} 6
函数调用之后,text已经被移动,不能继续使用。
但get_length()实际上只需要读取字符串长度,并不需要取得所有权。
更合理的写法:
1fn get_length(text: &String) -> usize { 2 text.len() 3} 4 5fn main() { 6 let text = String::from("Hello Rust"); 7 let length = get_length(&text); 8 println!("长度:{}", length); 9 println!("内容:{}", text); 10} 11
这里:
1get_length(&text) 2
只是把text借给函数使用。
Borrow与Move的区别
Move:
1let first = String::from("Rust"); 2let second = first; 3
结果:
1first失效 2second取得所有权 3
Borrow:
1let first = String::from("Rust"); 2let second = &first; 3
结果:
1first仍然拥有数据 2second只是借用数据 3
可以简单记住:
Move是“把东西给你”。
Borrow是“借给你看看”。
函数参数中的Borrow
实际开发中,借用最常见的地方就是函数参数。
例如:
1fn print_user(name: &String) { 2 println!("用户名:{}", name); 3} 4
调用:
1fn main() { 2 let name = String::from("Alice"); 3 print_user(&name); 4 print_user(&name); 5 println!("{}", name); 6} 7
同一个变量可以被反复借用,因为所有权没有离开原变量。
&String与&str
如果函数只需要读取字符串,一般更推荐:
1&str 2
而不是:
1&String 2
例如:
1fn print_name(name: &str) { 2 println!("用户名:{}", name); 3} 4 5fn main() { 6 let first = String::from("Tom"); 7 let second = "Alice"; 8 print_name(&first); 9 print_name(second); 10} 11
&str更加通用,它既能接受:
1String 2
的借用,也能接受字符串字面量。
所以只读字符串参数通常推荐:
1fn function(text: &str) 2
多个不可变引用
Rust允许同时存在多个不可变引用。
例如:
1fn main() { 2 let text = String::from("Rust"); 3 let first = &text; 4 let second = &text; 5 let third = &text; 6 println!("{}", first); 7 println!("{}", second); 8 println!("{}", third); 9} 10
这是安全的。
因为多个不可变引用只能读取数据,不会修改数据,因此不会产生数据冲突。
可以简单表示为:
1 ┌─ first 2text ─┼─ second 3 └─ third 4
所有引用都只是读取同一个值。
不可变引用不能修改数据
下面的代码会报错:
1fn main() { 2 let text = String::from("Rust"); 3 let reference = &text; 4 reference.push_str(" Language"); 5} 6
原因是:
1reference 2
类型为:
1&String 2
它只是不可变借用。
不可变借用的规则是:
可以读取,不能修改。
如果需要通过引用修改数据,需要使用:
1&mut T 2
这就是下一篇要重点讲解的“可变借用”。
借用Vec
Vec也可以通过引用借用:
1fn print_numbers(numbers: &Vec<i32>) { 2 println!("{:?}", numbers); 3} 4 5fn main() { 6 let numbers = vec![10, 20, 30]; 7 print_numbers(&numbers); 8 println!("{:?}", numbers); 9} 10
不过函数只需要读取元素时,更推荐切片:
1fn print_numbers(numbers: &[i32]) { 2 for number in numbers { 3 println!("{}", number); 4 } 5} 6
这样既可以传入Vec:
1let numbers = vec![1, 2, 3]; 2print_numbers(&numbers); 3
也可以传入数组:
1let numbers = [1, 2, 3]; 2print_numbers(&numbers); 3
借用结构体
结构体同样可以通过引用传入函数。
1#[derive(Debug)] 2struct User { 3 name: String, 4 age: u32, 5} 6 7fn print_user(user: &User) { 8 println!("姓名:{}", user.name); 9 println!("年龄:{}", user.age); 10} 11 12fn main() { 13 let user = User { 14 name: String::from("Tom"), 15 age: 20, 16 }; 17 print_user(&user); 18 println!("{:?}", user); 19} 20
print_user()不会取得User的所有权,因此调用后user仍然有效。
在业务项目中,大型结构体通常不会为了读取而频繁Move或Clone,而是通过:
1&User 2
借用。
引用不会拥有数据
看下面代码:
1fn main() { 2 let text = String::from("Rust"); 3 let reference = &text; 4 println!("{}", reference); 5} 6
当reference离开作用域时,不会释放字符串数据。
真正负责释放数据的是:
1text 2
因为它才是所有者。
可以理解为:
1text 2 | 3 └──拥有──> "Rust" 4 ↑ 5 | 6reference只是指向 7
解引用*
引用保存的是对数据的访问方式,如果想获得引用指向的值,可以使用:
1* 2
称为解引用。
示例:
1fn main() { 2 let number = 100; 3 let reference = &number; 4 println!("{}", *reference); 5} 6
这里:
1*reference 2
得到引用指向的i32值。
例如:
1fn main() { 2 let number = 10; 3 let reference = &number; 4 assert_eq!(number, *reference); 5} 6
number是:
1i32 2
reference是:
1&i32 2
*reference再次得到:
1i32 2
为什么println!经常不需要*
可能会发现:
1println!("{}", reference); 2
也能正常输出。
这是因为Rust很多场景会自动进行解引用或格式化处理,不需要每次显式写:
1*reference 2
类似地:
1user.name 2
即使user是&User,Rust也通常能够自动处理字段访问。
借用的作用域
引用不能比它指向的数据活得更久。
错误示例:
1fn create_reference() -> &String { 2 let text = String::from("Rust"); 3 &text 4} 5
这是不允许的。
因为函数结束时:
1text 2
会被释放。
如果允许返回:
1&text 2
那么返回的引用将指向已经被释放的内存。
这种引用叫:
1悬空引用 Dangling Reference 2
Rust编译器会直接阻止这种代码。
悬空引用
在C/C++中可能出现类似:
1创建数据 2↓ 3取得指针 4↓ 5释放数据 6↓ 7继续访问指针 8
这可能造成严重内存错误。
Rust通过借用检查确保:
引用永远不能比被引用的数据活得更久。
因此Rust从编译阶段避免大量悬空指针问题。
借用作用域可以提前结束
看下面代码:
1fn main() { 2 let text = String::from("Rust"); 3 let reference = &text; 4 println!("{}", reference); 5 println!("{}", text); 6} 7
现代Rust借用检查器通常会判断:
1reference 2
最后一次使用的位置。
当:
1println!("{}", reference); 2
结束后,如果后面不再使用reference,这次借用也可以认为已经结束,而不一定要等整个代码块结束。
这让Rust的借用规则更加灵活。
Borrow Checker是什么
Rust编译器中负责检查借用是否合法的机制通常称为:
1Borrow Checker 2
借用检查器主要保证:
- 引用指向的数据仍然有效
- 不会出现悬空引用
- 不会发生不安全的同时读写
- 可变访问不会产生数据竞争
例如下面的代码无法通过编译:
1fn main() { 2 let reference; 3 { 4 let text = String::from("Rust"); 5 reference = &text; 6 } 7 println!("{}", reference); 8} 9
因为text在内部代码块结束时已经被释放,但reference还想继续访问它。
借用与所有权的关系
假设:
1let text = String::from("Rust"); 2let reference = &text; 3
关系是:
1text 2拥有String 3| 4└── reference借用text 5
借用不会改变真正的所有者。
所以:
1drop(reference); 2
不会释放String。
最终仍然由text负责释放内存。
需要注意,对于Copy类型的引用,显式drop(reference)通常没有实际意义,因为引用本身可以直接复制。
借用与Clone的选择
假设函数只需要打印字符串。
方案一:
1fn print_text(text: String) { 2 println!("{}", text); 3} 4 5fn main() { 6 let text = String::from("Rust"); 7 print_text(text.clone()); 8} 9
虽然可以工作,但为了打印一次就复制整个字符串并不划算。
更好的方式:
1fn print_text(text: &str) { 2 println!("{}", text); 3} 4 5fn main() { 6 let text = String::from("Rust"); 7 print_text(&text); 8} 9
原则:
只读取数据时优先Borrow。
真正需要独立副本时再Clone。
实战:计算订单总价
1#[derive(Debug)] 2struct Order { 3 product: String, 4 price: f64, 5 count: u32, 6} 7 8fn calculate_total(order: &Order) -> f64 { 9 order.price * order.count as f64 10} 11 12fn main() { 13 let order = Order { 14 product: String::from("Rust Book"), 15 price: 99.0, 16 count: 3, 17 }; 18 let total = calculate_total(&order); 19 println!("订单:{:?}", order); 20 println!("总价:{:.2}", total); 21} 22
函数只读取订单,所以使用:
1&Order 2
而不是取得整个Order的所有权。
实战:查找最大值
1fn find_max(numbers: &[i32]) -> Option<&i32> { 2 numbers.iter().max() 3} 4 5fn main() { 6 let numbers = vec![10, 88, 35, 99, 20]; 7 match find_max(&numbers) { 8 Some(max) => println!("最大值:{}", max), 9 None => println!("没有数据"), 10 } 11 println!("{:?}", numbers); 12} 13
函数接收:
1&[i32] 2
并返回:
1Option<&i32> 2
整个过程中不需要复制Vec中的数据。
常见错误
函数只读却取得所有权
不推荐:
1fn print_user(user: User) 2
如果只查看用户信息,更推荐:
1fn print_user(user: &User) 2
为了继续使用原变量到处clone
不推荐:
1process(data.clone()); 2
如果process()只是读取数据,应改成接受引用。
返回局部变量的引用
错误:
1fn get_name() -> &String { 2 let name = String::from("Tom"); 3 &name 4} 5
局部变量离开函数后会被释放,不能返回指向它的引用。
试图通过不可变引用修改
错误:
1let text = String::from("Rust"); 2let reference = &text; 3reference.push('!'); 4
需要修改时必须使用可变借用。
把引用误认为新的所有者
1let second = &first; 2
并不会转移或复制底层数据,只是借用。
Borrow最佳实践
只读函数参数优先引用
字符串:
1fn print(text: &str) 2
集合:
1fn sum(values: &[i32]) 2
结构体:
1fn show(user: &User) 2
不需要独立数据时不要Clone
Borrow通常比复制大型String、Vec、HashMap更高效。
业务实体负责拥有数据
例如:
1struct User { 2 name: String, 3} 4
User拥有用户名,而临时处理函数使用:
1&User 2
或:
1&str 2
访问。
API通过类型表达意图
1fn save(user: User) 2
通常表达“函数接管User”。
1fn show(user: &User) 2
表达“函数只临时访问User”。
良好的Rust API应该让所有权意图一眼就能看出来。
本章小结
Borrow让Rust可以在不转移所有权的情况下安全访问数据。
本文学习了:
- Borrow表示临时借用数据
- 使用
&T创建不可变引用 - 借用不会改变数据所有者
- 多个不可变引用可以同时存在
- 不可变引用只能读取数据
- 函数参数可以通过引用避免Move
- 字符串参数通常推荐
&str - 集合参数通常推荐切片
&[T] - 结构体可以通过
&Struct借用 - 使用
*进行解引用 - 引用不能比原数据活得更久
- Rust通过Borrow Checker防止悬空引用
- Borrow通常比无意义的Clone更加高效
可以记住:
Ownership决定谁拥有数据。
Move负责转移所有权。
Borrow允许临时访问数据而不取得所有权。
如果函数只是读取数据,优先考虑借用而不是Move或Clone。
下一篇预告
下一篇我们将继续学习:
Rust可变借用详解:&mut T与安全修改数据
内容包括:
- 什么是可变借用
&mut T基本语法- 通过可变引用修改数据
- 为什么同一时间只能有一个可变引用
- 不可变借用与可变借用冲突
- 可变借用作用域
- 函数参数中的
&mut - String、Vec和Struct的可变借用
- Reborrow重新借用
- Borrow Checker常见报错
- 可变借用实战与最佳实践
《Rust Borrow借用详解:不转移所有权访问数据》 是转载文章,点击查看原文。

