引用的规则:
- 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
- 引用必须总是有效的。
下面的引用学习都是围绕上面的两条规则来展开
一:引用和借用
下面我们通过一个例子展示引用,引用并不获取入参的所有权
rust
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.len()
}
运行结果:

在calculate_length函数的定义中,入参s的类型是&string,& 符号就是 引用,它们允许你使用值但不获取其所有权

变量 s
有效的作用域与函数参数的作用域一样,不过当引用停止使用时并不丢弃它指向的数据,因为我们没有所有权。当函数使用引用而不是实际值作为参数,无需返回值来交还所有权,因为就不曾拥有所有权。
我们将创建一个引用的行为称为 借用 (borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。
引用默认是不可变的,如果尝试要修改引用的变量会报错
rust
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
运行结果:

那如果我想在函数中修改引用的变量,有没有办法呢
二:可变引用
rust
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}",s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
运行结果:

可以看到在函数中修改可变引用也就等同于修改了变量s
不过可变引用有一个很大的限制:在同一时间,只能有一个对某一特定数据的可变引用。尝试创建两个可变引用的代码将会失败:
rust
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
}
运行结果:

我们不能在同一时间多次将 s
作为可变变量借用。第一个可变的借入在 r1
中,并且必须持续到在 println!
中使用它,但是在那个可变引用的创建和它的使用之间,我们又尝试在 r2
中创建另一个可变引用,它借用了与 r1
相同的数据。
防止同一时间对同一数据进行多个可变引用的限制允许可变性,不过是以一种受限制的方式允许。
这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争 (data race)类似于竞态条件,它由这三个行为造成:
- 两个或更多指针同时访问同一数据。
- 至少有一个指针被用来写入数据。
- 没有同步数据访问的机制。
注意:以上三个行为同时发生才会造成数据竞争,而不是单一行为。
可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用,只是不能 同时 拥有
rust
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
let r2 = &mut s;
类似的规则也存在于同时使用可变与不可变引用中
rust
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 有问题
println!("{}, {}, and {}", r1, r2, r3);
不能在拥有不可变引用的同时拥有可变引用
注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。例如,因为最后一次使用不可变引用(println!
),发生在声明可变引用之前,所以如下代码是没有问题的
rust
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
println!("{} and {}", r1, r2);
// 此位置之后 r1 和 r2 不再使用
let r3 = &mut s; // 没问题
println!("{}", r3);
}
运行结果:

不可变引用 r1
和 r2
的作用域在 println!
最后一次使用之后结束,这也是创建可变引用 r3
的地方。它们的作用域没有重叠,所以代码是可以编译的。编译器在作用域结束之前判断不再使用的引用的能力被称为非词法作用域生命周期(Non-Lexical Lifetimes,简称 NLL)。
三:悬垂引用
在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针 (dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
下面是一个悬垂引用,Rust 会通过一个编译时错误来避免

下面看看s在内存中的表现
rust
fn dangle() -> &String { // dangle 返回一个字符串的引用
let s = String::from("hello"); // s 是一个新字符串
&s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
// 危险!
因为 s
是在 dangle
函数内创建的,当 dangle
的代码执行完毕后,s
将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的 String