目录
2. 引用与借用
2.1 可变(mutable)引用
2.2 悬空(dangling)引用
2.3 引用的规则总结
2. 引用与借用
上一章节中提到,所有权在函数调用中的转移,函数返回必须同时返还所有权,才能使函数调用后能继续使用某个变量(如上一章节中的String变量)。
Rust支持引用类型,引用类型不需要转移(move)所有权。 引用类似一个指针,能利用该指针指向的地址进行访问其它变量所有权的数据。但与指针不同,引用保证指向一个特定类型的有效值。 如下:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize { // s是一个String的引用类型
s.len()
} // 至此,s离开其作用域,但s不具有其值的所有权,因此Rust不会自动调用drop进行销毁
可以看到,函数形参使用了&String取代String类型,函数实参使用&s1取代了s1。 这种&号就是表示引用,允许引用到某个值,但并不拥有该值的所有权。如下图原理:
引用(&)的反向操作是解引用,使用*操作符完成。在第8章节中将介绍解应用操作符的使用,并在第15章节详细介绍解应用。
具体地展开细看,如下:
let s1 = String::from("hello");
let len = calculate_length(&s1);
&s1语法创建了一个指向但不拥有s1的引用,因此该应用在不使用后,不会调用drop。同样地,函数声明的原型也使用&来标识其形参s是String的引用类型。使用引用类型的函数,就不需要在函数返回时,返回该形参变量以归还所有权,因为引用本身就没有拿到所有权。
我们称引用这种行为为borrowing(借用),因为不具有所有权,不能通过引用来修改其值。
如下:
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
编译时会报错:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
7 | fn change(some_string: &String) {
| ------- help: consider changing this to be a mutable reference: `&mut String`
8 | some_string.push_str(", world");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` due to previous error
如同变量默认是不可变的,引用也是不可改变的。不允许通过引用来更改其指向的值。
2.1 可变(mutable)引用
通过mut关键词可以运行通过引用来修改一个“借用”的值。如下:
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
1)函数原型定义成&mut的可变引用String类型
2)函数调用传递可变的引用实参&mut s
3) 原始字符串类型定义成mut的可变字符串类型
可变引用有一个很大的限制:特定时间,只能有一个对特定数据的可变引用。 如下试图创建两个对s的可变引用将失败:
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
编译时将报错:
# cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{}, {}", r1, r2);
| -- first borrow later used here
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` due to previous error
不能在同一时间多次将s借为可变的,第一个可变的borrow在r1中,必须持续到println中使用它。但是在这个可变引用的创建和它的使用之间,在r2中创建另一个可变引用,它借用了与r1相同的数据。
禁止同时对同一数据进行多个可变引用的限制可以来控制值的可改变性。这对于Rust新手来说,是比较难以掌握的,因为大多数语言允许变量随时随地被改动。这个限制的好处是:Rust可以在编译时即消除数据竞争,这是Rust的一大特色。 数据竞争类似于竞争条件(Race Condition),当出现以下三种行为时发生:
-
两个或多个指针相同时刻访问相同的数据
-
至少使用其中一个指针执行数据写入操作
-
没有任何机制用来来同步对数据的访问
数据竞争会导致未定义的行为,在运行时追踪时,非常难以诊断和修复数据竞争带来的错误。Rust通在编译时,报错拒绝数据竞争的代码来避免数据竞争问题的出现。
可以利用作用域来控制可变引用的生命周期,避免同时多个可变引用,如下:
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 离开作用域,销毁。
let r2 = &mut s; // 可以定义新的可变引用到s
与可变引用不同的是,Rust允许同时多个指向相同数据的不可变引用。 如下:
let mut s = String::from("hello");
let r1 = &s; // 合法
let r2 = &s; // 合法
let r3 = &mut s; // 非法
println!("{}, {}, and {}", r1, r2, r3);
编译错误如下:
# cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:6:14
|
4 | let r1 = &s; // no problem
| -- immutable borrow occurs here
5 | let r2 = &s; // no problem
6 | let r3 = &mut s; // BIG PROBLEM
| ^^^^^^ mutable borrow occurs here
7 |
8 | println!("{}, {}, and {}", r1, r2, r3);
| -- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error
当存在指向相同值的不可变引用时,也不能有可变引用。
注意,引用的作用域从它被引入的地方开始,一直延续到该引用最后一次被使用。 例如,如下代码是可以编译成功的,因为不可变引用的最后一次使用println!,在引入可变引用之前发生:
let mut s = String::from("hello");
let r1 = &s; // 合法
let r2 = &s; // 合法
println!("{} and {}", r1, r2);
// r1和r2之后没有被使用到
let r3 = &mut s; // r1,r2后面无使用,这里可以编译成功
println!("{}", r3);
不可变引用r1和r2的作用域只到在println!最后使用它们的地方,也就是在可变引用r3创建之前。
这些作用域不重叠,因此s3可变引用的定义代码是合法的。编译器能判断出某个引用在作用域结束之前就不再被使用的能力被称为非词法生命周期(Non-Lexical lifetime,简称NLL),可以参考如下链接:
https://blog.rust-lang.org/2018/12/06/Rust-1.31-and-rust-2018.html#non-lexical-lifetimes
尽管使用借用导致的错误可能令编程人员沮丧,但Rust编译器在早期(在编译时而非运行时)发现并指出了潜在的错误,准确地指出了问题所在,避免了在运行时追踪非期望结果的复杂。
2.2 悬空(dangling)引用
在有指针的编程语言中,当通过指针释放内存,同时保留指向该内存的指针时,错误地保留一个非法的指针称作悬空指针,其引用内存可能已经分配他用。
在Rust中,编译器保证了引用永远不会是悬空引用。如果对某些数据有引用,编译器将确保数据不会在对数据的引用之前离开作用域(所有权不会被转移他用)。 如下示例:
fn main() {
let reference_to_nothing = dangle(); // reference_to_nothing是借用,
// 没有所有权
}
fn dangle() -> &String {
let s = String::from("hello");
&s // 返回一个到字符串s的引用,主调函数产生一次borrowing
} // s离开作用域,被drop,内存被释放回收
当dangle()函数返回时,字符串s实际已经被销毁。返回的借用的引用变成“悬挂”引用,编译器报错如下:
# cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
|
5 | fn dangle() -> &'static String {
| ~~~~~~~~
For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` due to previous error
2.3 引用的规则总结
-
任何时刻,可以有一个可变的引用,或者多个不可变的引用
-
引用必须是有效的(不能是悬挂的)
关于作者:
犇叔,浙江大学计算机科学与技术专业,研究生毕业,而立有余。先后在华为、阿里巴巴和字节跳动,从事技术研发工作,资深研发专家。主要研究领域包括虚拟化、分布式技术和存储系统(包括CPU与计算、GPU异构计算、分布式块存储、分布式数据库等领域)、高性能RDMA网络协议和数据中心应用、Linux内核等方向。
专业方向爱好:数学、科学技术应用
关注犇叔,期望为您带来更多科研领域的知识和产业应用。
内容坚持原创,坚持干货有料。坚持长期创作,关注犇叔不迷路