rust-参考与借用
参考与借用
在清单4-5中的元组代码的问题在于,我们必须将String
返回给调用函数,这样我们才能在调用calculate_length
之后继续使用String
,因为String
已经被移动到了calculate_length
中。相反,我们可以提供一个对String
值的引用。引用类似于指针,它是一个地址,我们可以沿着它访问存储在该地址的数据;这些数据由其他某个变量拥有。与指针不同的是,引用在引用的生命周期内保证指向一个特定类型的有效值。
以下是如何定义和使用一个以对象的引用作为参数而不是获取值的所有权的calculate_length
函数:
文件名:src/main.rs
fn main() {let s1 = String::from("hello");let len = calculate_length(&s1);println!("'{}'的长度是{}。", s1, len);
}
fn calculate_length(s: &String) -> usize {s.len()
}
首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们将&s1
传递给calculate_length
,在其定义中,我们接受&String
而不是String
。这些&
符号代表引用,它们允许你在不获取所有权的情况下引用某个值。图4-6展示了这一概念。
图4-6:一个指向String s1
的&String s
的示意图
注意:使用&
来引用的相反操作是解引用,这可以通过解引用运算符*
来完成。我们将在第8章看到一些解引用运算符的用法,并在第15章详细讨论解引用的细节。
让我们更仔细地看看这里的函数调用:
let s1 = String::from("hello");let len = calculate_length(&s1);
&s1
的语法让我们创建了一个引用,它指向s1
的值,但并不拥有它。因为引用并不拥有它,所以当引用不再被使用时,它所指向的值不会被释放。
同样,函数的签名使用&
来表明参数s
的类型是一个引用。我们来添加一些解释性的注释:
fn calculate_length(s: &String) -> usize { // s是一个指向String的引用s.len()
} // 在这里,s的作用域结束了。但由于s并不拥有它所引用的内容,所以值不会被释放。
变量s
的有效作用域与任何函数参数的作用域相同,但引用所指向的值在s
不再被使用时不会被释放,因为s
并不拥有它。当函数的参数是引用而不是实际值时,我们不需要返回值以交还所有权,因为我们从未拥有过所有权。
我们将创建引用的行为称为借用。就像在现实生活中一样,如果一个人拥有某样东西,你可以从他那里借来。当你用完后,你必须归还。你并不拥有它。
那么,如果我们尝试修改我们正在借用的内容会发生什么呢?尝试清单4-6中的代码。剧透警告:它无法工作!
文件名:src/main.rs
这段代码无法编译!
fn main() {let s = String::from("hello");change(&s);
}fn change(some_string: &String) {some_string.push_str(", world");
}
清单4-6:尝试修改一个被借用的值
这是错误信息:
$ cargo runCompiling 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|
8 | some_string.push_str(", world");| ^^^^^^^^^^^ `some_string`是一个`&`引用,因此它所引用的数据不能被借用为可变的|
help: 考虑将其改为可变引用|
7 | fn change(some_string: &mut String) {| +++
有关此错误的更多信息,请尝试rustc --explain E0596
。
错误:由于之前的1个错误,无法编译ownership
(二进制文件“ownership”)
正如变量默认是不可变的一样,引用也是不可变的。我们不允许修改我们引用的内容。
可变引用
我们可以通过一些小的调整来修复清单4-6中的代码,从而允许我们修改一个被借用的值。这些调整使用的是可变引用:
文件名:src/main.rs
fn main() {let mut s = String::from("hello");change(&mut s);
}fn change(some_string: &mut String) {some_string.push_str(", world");
}
首先,我们将s
改为mut
。然后,我们在调用change
函数时使用&mut s
创建一个可变引用,并更新函数签名以接受一个可变引用some_string: &mut String
。这使得change
函数将修改它借用的值这一行为变得非常清晰。
可变引用有一个重要的限制:如果你有一个对某个值的可变引用,那么你不能有其他对该值的引用。这段尝试为s
创建两个可变引用的代码将无法通过编译:
文件名:src/main.rs
这段代码无法编译!
let mut s = String::from("hello");let r1 = &mut s;
let r2 = &mut s;println!("{}, {}", r1, r2);
这是错误信息:
$ cargo runCompiling 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;| ------ 第一个可变借用发生在这里
5 | let r2 = &mut s;| ^^^^^^ 第二个可变借用发生在这里
6 |
7 | println!("{}, {}", r1, r2);| -- 第一个借用在此处后续使用有关此错误的更多信息,请尝试`rustc --explain E0499`。
错误:由于之前的1个错误,无法编译`ownership`(二进制文件“ownership”)
这个错误表明这段代码是无效的,因为我们不能同时多次将s
借用为可变的。第一个可变借用在r1
中,必须持续到它在println!
中被使用为止,但在那个可变引用创建和使用之间,我们试图在r2
中创建另一个可变引用,它与r1
借用相同的数据。
防止同时对相同数据进行多次可变引用的限制允许进行修改,但这种修改是受到严格控制的。新Rustaceans(Rust新手)常常会在这个问题上挣扎,因为大多数语言允许你在任何时候进行修改。这种限制的好处是Rust可以在编译时防止数据竞争。数据竞争类似于竞态条件,当出现以下三种行为时就会发生:
- 两个或更多的指针同时访问相同的数据。
- 至少有一个指针被用来写入数据。
- 没有机制被用来同步对数据的访问。
数据竞争会导致未定义行为,并且在运行时试图追踪它们时很难诊断和修复;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);
这是错误信息:
$ cargo runCompiling 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; // 没有问题| -- 不可变借用发生在这里
5 | let r2 = &s; // 没有问题
6 | let r3 = &mut s; // 大问题| ^^^^^^ 可变借用发生在这里
7 |
8 | println!("{}, {}, and {}", r1, r2, r3);| -- 不可变借用在此处后续使用有关此错误的更多信息,请尝试`rustc --explain E0502`。
错误:由于之前的1个错误,无法编译`ownership`(二进制文件“ownership”)
呼!我们也不能在有对同一个值的不可变引用的同时拥有一个可变引用。
不可变引用的使用者不会期望值在他们不知情的情况下突然改变!然而,允许多个不可变引用是合理的,因为那些只是读取数据的人无法影响其他人的读取。
注意,引用的作用域从它被引入的地方开始,并持续到最后一次使用该引用的地方。例如,这段代码可以编译,因为不可变引用的最后一次使用是在println!
中,这在可变引用被引入之前:
let mut s = String::from("hello");let r1 = &s; // 没有问题
let r2 = &s; // 没有问题
println!("{r1} and {r2}");
// 变量r1和r2在此点之后将不再被使用。let r3 = &mut s; // 没有问题
println!("{r3}");
不可变引用r1
和r2
的作用域在它们最后一次被使用的println!
之后结束,这在可变引用r3
被创建之前。这些作用域没有重叠,所以这段代码是被允许的:编译器可以判断出在作用域结束之前引用不再被使用。
接下来,我们将看看另一种引用类型:切片。