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