开发环境
- Windows 10
- Rust 1.55.0
- VS Code 1.60.2
项目工程
这里继续沿用上次工程rust-demo
引用和借用
上述章节中的元组代码的问题是,我们必须将String返回给调用函数,这样我们仍然可以在调用之后使用String来calculate_length,因为String被移动到计calculate_length中。
下面是如何定义和使用具有对对象的引用作为参数的calculate_length函数,而不是获取该值的所有权:
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()
}
首先,注意变量声明和函数返回值中的所有元组代码都不见了。其次,请注意,我们将&s1传递到calculate_length中,在其定义中,我们采用&string而不是String。
这些符号是引用,它们允许您引用某些值,而不需要拥有它的所有权。指向String s1的&String s的关系图如下:
注意:
与使用&的引用相反的是取消引用,这是通过取消引用操作符*完成的。
让我们仔细看看这里的函数调用
let s1 = String::from("hello");
let len = calculate_length(&s1); // 引用
&s1语法允许我们创建引用,引用s1的值,但不拥有它。因为它不拥有它,所以当引用超出范围时,它所指向的值不会被删除。
同样,函数的签名使用&指示参数s的类型是引用。让我们添加一些解释性注释:
fn calculate_length(s: &String) -> usize { // s是字符串引用
s.len()
} // 这里,s超出了范围。 但是因为它对它所指代的东西没有所有权,所以什么都不会发生。
变量s有效的作用域与任何函数参数的作用域相同,但当它超出作用域时,我们不会删除引用指向的值,因为我们没有所有权。当函数将引用作为参数而不是实际值时,我们将不需要返回值来返回所有权,因为我们从来没有所有权。
我们称有引用为函数参数借用。在现实生活中,如果一个人拥有某物,你可以向他们借。当你完成的时候,你必须把它还给我。
那么,如果我们试图修改我们正在借来的东西,会发生什么呢?尝试上例中的代码。出现警报:它不起作用!
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
运行测试
正如变量在默认情况下是不可变的一样,引用也是不可变的。我们不允许修改我们所引用的东西。
可变引用
我们只需稍作调整,就可以修复上述示例中的代码的错误。
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) { // 关键字mut
some_string.push_str(", world");
}
首先,我们必须改变s的类型为mut。然后,我们必须使用&mut s创建一个可变引用,并接受some_string:&mut字符串的可变引用。
但是可变引用有一个很大的限制:对特定范围内的特定数据只能有一个可变的引用。下面的代码会出错:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s; // r1引用可变字符串s
let r2 = &mut s; // r2引用可变字符串s
println!("{}, {}", r1, r2);
}
运行测试
这一限制允许可变性,但以一种非常可控的方式。这是新Rust程序员们苦苦挣扎的地方,因为大多数语言都允许你随时变异。
拥有这一限制的好处是,Rust可以在编译时防止数据竞争。数据竞争类似于竞争条件,并在这三种行为发生时发生。
- 两个或多个指针同时访问相同的数据。
- 至少有一个指针被用来写入数据。
- 没有用于同步数据访问的机制。
数据竞赛会导致未定义的行为,当您试图在运行时跟踪它们时,很难诊断和修复它们;Rust防止了这个问题的发生,因为它甚至不会用数据竞争来编译代码!
与往常一样,我们可以使用花括号创建一个新的范围,允许多个可变引用,而不是同时引用:
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1超出了这里的范围,所以我们可以在没有问题的情况下做一个新的参考
let r2 = &mut s;
}
对于组合可变的和不可变的引用,也存在类似的规则。参考如下示例:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 这里会出错
println!("{}, {}, and {}", r1, r2, r3);
}
运行测试
当我们有一个不变的引用时,我们也不能有一个可变的引用。不可变引用的用户不会期望值突然从它们下面改变!但是,多个不可变的引用是可以的,因为任何刚刚读取数据的人都没有能力影响其他人对数据的读取。
请注意,引用的作用域从引入它的位置开始,并一直持续到上次使用该引用。例如,这段代码将编译,因为最后一次使用不可变引用发生在引入可变引用之前。
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之前。这些作用域不重叠,因此允许使用此代码。
尽管借用错误有时可能令人沮丧,但请记住,是Rust编译器及早指出了潜在的错误(在编译时而不是在运行时),并向您展示了问题所在。然后你就不需要找出为什么你的数据不是你想象的那样了。
悬空引用
在有指针的语言中,很容易错误地创建一个悬空指针,该指针通过释放一些内存,同时保留指向该内存的指针,来引用内存中可能给其他人的位置。相比之下,在Rust中,编译器保证引用永远不会是悬空引用:如果您有对某些数据的引用,编译器将确保数据在引用数据之前不会超出范围。
让我们尝试创建一个悬空引用,Rust将通过编译时错误来防止这种引用。
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle返回对字符串的引用。
let s = String::from("hello"); // s是个新字符串
&s // 返回对字符串s的引用
} // s超出了范围,被丢弃了。它的内存信息消失了。
运行测试
此错误消息引用了我们尚未讨论的一个特性:lifetime。我们将在后面论lifetime。
因为s是在dangle中创建的,所以当dangle完成时,s将被释放。但我们试图返回一个引用。这意味着此引用将指向无效String。
这里的解决方案是直接返回String。如下
fn main() {
let string = no_dangle();
}
fn no_dangle() -> String { // 返回类型为字符串
let s = String::from("hello");
s // 直接返回字符串
}
这没有任何问题。所有权被移除,任何东西都不会被释放。
引用规则
- 在任何给定的时间,您可以有一个可变引用或任意数量的不可变引用。
- 引用必须始终有效。
本章重点
- 引用
- 借用
- 可变引用
- 引用的规则和注意事项