Rust: 借用
前面我们介绍了所有权的相关概念,下面用一段代码简单回顾一下:
fn main() {
let a = String::from("hello");
// a的所有权转移到了b
let b = a;
// b的所有权转移到了函数内部
let len = get_len(b);
// 此时只能访问len变量
println!("{}", len);
// 报错:a和b所有权已转移
println!("{} {}", a, b);
}
fn get_len(s: String) -> usize {
s.len()
}
变量丢掉自己的所有权很容易,可要想重新获得,那就得费点功夫了,比如说在 get_len() 函数执行完毕,必须将参数变量 s 作为结果,原封不动地返回。
要是每个函数都这样写,岂不是要疯了?有没有别的的方式,可以简化这种繁琐的过程呢,答案是 借用
。
在变量前面加一个 &
符号,就能用来表示借用。借用只会 租借
原变量的所有权,在获得数据访问能力的同时,不会剥夺原变量对数据的所有权。
我们来看下面代码:
fn main() {
let s1 = String::from("hello");
// &表示借用
let s2 = &s1;
// 变量s1和s2都能访问
// 打印结果 "hello" "hello"
println!("{:?} {:?}", s1, s2);
}
在调用函数时,也是使用同样的手法:
fn main() {
let s = String::from("hello");
// 传递借用参数
let len = get_len(&s);
// 变量s依然能够访问
// 打印结果 "hello" 5
println!("{:?} {:?}", s, len);
}
// 这里不会引起所有权变化
fn get_len(s: &String) -> usize {
s.len()
}
那么通过借用生成的变量,究竟是一种什么样的存在呢?
实际上,借用会产生一个指针,这个指针会指向原变量,通过原变量,最终能访问到指定内存地址的数据。所以,对于借用生成的变量,我们称之为 借用指针
。
借用指针
有时候也被称为 引用
,两者要表达的含义是一致的。为了表述清晰,本文将统一使用 借用
和 借用指针
这两个词。
上面我们看到的借用是 只读
的,也就是说,你只能通过它访问数据,不能通过它对数据进行更改。原因很简单,如果借用指针偷偷修改了数据,原变量对此一无所知,那是要出大问题的,编译器绝不允许这种情况发生。
但如果在某些场景下,确实需要通过借用指针来修改数据,那我们就必须为 原变量
和 借用指针
都加上 mut
关键字修饰,显式地告诉编译器,此处的借用指针可以修改数据,我们称这种借用为 可变
借用,且看下面代码:
fn main() {
// 1. 源头就必须是mutable的
let mut s = String::from("hello");
// 2. 传递参数时必须有mut修饰
change(&mut s);
// 打印结果 "hello world"
println!("{:?}", s);
}
// 3. 形参也必须有mut修饰
fn change(s: &mut String) {
s.push_str(" world");
}
现在我们来对比一下 只读借用
和 可变借用
的不同:
- 只读借用使用
&
来表示;可变借用使用&mut
来表示。 - 只读借用只可访问数据,不能更改数据;可变借用既能访问数据,也可以对数据进行更改。
既然只读借用只可访问数据,不能修改数据,那么多个借用就可以共享一份数据,这是内存安全的:
fn main() {
let s = String::from("hello");
let a = &s;
let b = &s;
println!("{} {} {}", s, a, b);
}
反之,可变借用就不能共享,&mut 型借用是独占的,在指定作用域中,只要存在 &mut 型借用,就不允许其他借用出现,下面两段代码都将会编译失败:
fn main() {
let mut s = String::from("hello");
// 报错:同一时刻 两种类型的借用冲突
let a = &s;
let b = &mut s;
println!("{} {} {}", s, a, b);
}
fn main() {
let mut s = String::from("hello");
// 报错:同一时刻 最多一个可变借用
let a = &mut s;
let b = &mut s;
println!("{} {} {}", s, a, b);
}
这当然也是基于安全考虑的,当作用域中存在一个可变借用时,对其他借用来说,都是一种潜在的风险,说不定这家伙偷偷就把数据篡改了呢,所以 Rust 禁止这种情况的发生。
需要注意的是,在其他借用的生命周期已完结、资源被销毁后,这个限制会重新放开,此时我们可以继续在原变量上,定义新的可变借用:
fn main() {
let mut s = String::from("hello");
let a = &s;
println!("{}", a);
let b = &mut s;
println!("{}", b);
let c = &mut s;
println!("{}", c);
}
此外,当可变借用存在时,原变量会转为冻结状态,不可读写,只有在当前借用被销毁后,原变量才能恢复访问和修改:
fn main() {
let mut s1 = String::from("hello");
let s2 = &mut s1;
s2.push_str(" world");
println!("{}", s2);
// 变量s2销毁后才能访问s1
println!("{}", s1);
}
Rust 还规定,借用指针不能比原变量存活得更久,因为这样很容易出现 垂悬指针
的问题,就像下面这段代码一样:
fn main() {
let s = get_str();
println!("{}", s);
}
// 编译报错
fn get_str() -> &String {
let s = String::from("hello");
&s
}
在上面的 get_str() 方法内部,我们定义了一个变量,然后将它的借用指针作为结果返回,问题在于,这个变量在函数执行完即被销毁,留下一个孤零零的指针,访问不到数据,我们称其为垂悬指针,Rust 不允许这种异常现象发生,所以编译时会导致报错。
那怎么办呢,我们可以对代码稍作改进,不再返回借用指针,而是直接返回变量本身,将变量的所有权转移到外部,这样就可以继续访问了:
fn main() {
let s = get_str();
println!("{}", s);
}
fn get_str() -> String {
let s = String::from("hello");
// 交出所有权
s
}
最后,我们来总结一下借用的特点和规则:
- 借用只会租借变量的所有权,不能实际拥有所有权。
- 借用会产生一个指针,我们也可以称之为引用。
- 多个只读借用可以共享同一份数据。
- 可变借用在指定作用域中不能和其他借用共享数据,具有独占性。
- 当可变借用存在时,原变量会转为冻结状态,不可读写,直到可变借用被销毁。
- 借用指针不能比原变量存活得更久。
以上就是借用的相关内容了,里面的坑还是挺多的,初学者可能得多花点时间去理解,等这些规则都熟练掌握了,就能任性地去写 Rust 了。😃