Updated src/ownership/references_and_borrowing.md'.

This commit is contained in:
Hector PENG
2026-03-09 19:24:11 +08:00
parent f2a7ac6ced
commit 48d4ce459b
5 changed files with 90 additions and 100 deletions

View File

@@ -0,0 +1,6 @@
[package]
name = "ref_demo"
version = "0.1.0"
edition = "2024"
[dependencies]

View File

@@ -0,0 +1,9 @@
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}

View File

@@ -1,8 +0,0 @@
[package]
name = "references_demo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@@ -1,11 +0,0 @@
fn main() {
let mut s = String::from("你好");
let r1 = &s;
{
let r2 = &mut s;
r2.push_str(",世界");
}
println! ("s = {}, r2 = {}", s, r1);
}

View File

@@ -1,12 +1,8 @@
# 引用与借用
**References and Borrowing**
清单 4-5 中,元组代码的问题在于,我们必须将那个 `String` 返回给调用函数,以便咱们在调用 `calculate_length` 后,仍能使用这个 `String`,因为这个 `String` 已被迁移到 `calculate_length` 中。相反,我们可以提供一个到该 `String` 值的引用。*引用reference* 与指针类似,其是个我们可以沿着他,访问存储在其中数据的地址;其中的数据为其他变量所有。与指针不同的是,可以保证某个引用在其生命周期内,始终指向某个特定类型的有效值。
下面是如何定义和使用,以对象引用作为参数,而非取得值所有权的 `calculate_length` 函数:
[清单 4-5](./about_ownership.md#listing_4-5) 中元组代码下的问题在于,我们必须返回 `String` 给调用函数,以便咱们在调用 `calculate_length` 后仍能使用该 `String`,因为这个 `String` 已被迁移到 `calculate_length` 中。相反,我们可提供 `String` 值的引用。所谓 *引用reference*,就是一个指针,因为他是个地址,我们可顺着这个地址访问存储在该地址处的数据;该数据由其他变量所有。与指针不同,引用保证在该引用的声明周期内,指向特定类型的有效值。
下面是咱们将如何定义和使用 `calculate_length` 函数的方式,该函数有着到对象的引用作为参数,而非取得值的所有权:
文件名:`src/main.rs`
@@ -14,9 +10,9 @@
fn main() {
let s1 = String::from("hello");
let length = calculate_length(&s1);
let len = calculate_length(&s1);
println! ("字符串 '{}' 的长度为:{}", s1, length);
println! ("字符串 '{s1}' 的长度为:{len}");
}
fn calculate_length(s: &String) -> usize {
@@ -24,15 +20,15 @@ fn calculate_length(s: &String) -> usize {
}
```
首先,请注意变量声明和函数返回值中的所有元组代码都不见了。其次,注意我们 `&s1` 传入`calculate_length`,且在其定义中,我们使用了 `&String` 而不是 `String`。这 `&` 符合表示 *引用*他们允许咱们,在取得某个值所有权的情况下,对其进行引用。下图 4-5 描述了这一概念。
首先,请注意变量声明和函数返回值中的所有元组代码都没有了。其次,注意我们传递 `&s1``calculate_length` 中,而在其定义中,我们采用 `&String` 而不是 `String`。这两个 `&` 符合表示 *引用*,他们允许咱们指向某个值而无需取得他的所有权。下图 4-6 描述了这一概念。
<a name="f_4-6"></a>
![指向 `String s1` 的 `&String s` 图示](../images/Ch04_05.svg)
*图 4-5指向 `String s1` 的 `&String s` 图示*
**图 4-6**`&String s` 指向 `String s1` 的图示
> **注意**使用 `&` 进行引用相反的是,*解引用dereferencing*他是通过解引用操作符 `*` 实现的。我们将在第 8 章看到解引用操作符的一些用法,并在第 15 章讨论解引用的细节。
> **注意**:使用 `&` 进行引用的反面,即 *解引用dereferencing*其以解引用运算符 `*` 完成。我们将在第 8 章看到解引用运算符的一些用法,并在第 15 章讨论解引用的细节。
我们来仔细看看这里的函数调用:
@@ -45,26 +41,26 @@ fn calculate_length(s: &String) -> usize {
```
通过 `&s1` 这种语法,我们可以创建一个指向 `s1`但不拥有他的引用。由于这个引用不拥有 `s1`,因此其停止使用时,他所指向的值,不会被丢弃。
`&s1` 这种语法允许我们创建一个引用,*指向* `s1` 值但不拥有他。由于引用不拥有他,所以在引用停止使用后,其所指向的值将不被丢弃。
同样,那个函数的签名使用 `&` 表明参数 `s` 的类型是个引用。我们来添加一些解释性注
同样,函数的签名使用 `&` 表明参数 `s` 的类型是个引用。我们来添加一些解释性注
```rust
fn calculate_length(s: &String) -> usize { // s 是个到 String 的引用
fn calculate_length(s: &String) -> usize { // s 是个到 String 的引用
s.len()
} // 这里s 会超出作用域。但由于没有其指向的所有权,因此
// 不会被丢弃。
} // 这里s 会超出作用域。但由于 s 没有其指向内容的所有权,因此
// 那个 String 不会被丢弃。
```
变量 `s` 有效的作用域与任何的函数参数作用域相同,但当 `s` 停止使用时,引用所指向的值不会被丢弃,因为 `s` 没有所有权。当函数将引用而非具体值作为参数时,我们将无需为归还所有权返回值,因为我们从未拥有所有权。
变量 `s` 有效的作用域,与任何的函数参数作用域相同,但当 `s` 停止使用时,该引用所指向的值并不会丢弃,因为 `s` 没有所有权。当函数将引用而非实际值作为参数时,我们就不再需要返回值,来归还所有权,因为我们从未拥有过所有权
我们把创建引用的操作称为 *借用borrowing*。就像现实生活中一样,当某人拥有某样东西,咱们可以向他们借用。在用完后,咱们必须归还。咱们不拥有他
我们把这种创建出某个引用的行为,称为 *借用borrowing*。这如同现实生活中,如果某人拥有某样东西,咱们可以向其借用这件东西。用毕时,咱们必须归还。咱们并不拥有这件东西。
那么,如果我们试图修改借来的东西,会发生什么情况呢?请尝试下面清单 4-6 中的代码。剧透一下:这不会起作用!
那么,若我们试图修改借用的东西会发生什么呢?请尝试下面清单 4-6 中的代码。剧透预警:他不会工作!
<a name="listing_4-6"></a>
文件名:`src/main.rs`
```rust
@@ -77,22 +73,24 @@ fn main() {
fn change(some_string: &String) {
some_string.push_str(", world!");
}
```
*清单 4-6:尝试修改某个借用值*
**清单 4-6**:尝试修改借用的值
下面就是那个报错:
下面报错:
```console
$ cargo run
Compiling ownership_demo v0.1.0 (C:\tools\msys64\home\Lenny.Peng\rust-lang-zh_CN\projects\ownership_demo)
Compiling ref_demo v0.1.0 (/home/hector/rust-lang-zh_CN/projects/ref_demo)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src\main.rs:8:5
--> src/main.rs:8:5
|
8 | some_string.push_str(", world!");
| ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
| ^^^^^^^^^^^ `some_string` is a `&` reference, so it cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
@@ -100,19 +98,16 @@ help: consider changing this to be a mutable reference
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership_demo` (bin "ownership_demo") due to previous error
error: could not compile `ref_demo` (bin "ref_demo") due to 1 previous error
```
正如变量默认是不可变的一样,引用也是如此。我们不修改我们引用的东西
正如变量默认是不可变的一样,引用也是如此。我们不允许修改我们引用的内容
## 可变引用
**Mutable References**
只需稍作调整,使用 *可变引用mutable reference*,咱们即可修改某个借用值,便可修正清单 4-6 中的代码:
我们可以修复清单 4-6 中的代码,只需稍作调整,转而使用 *可变引用mutable reference*,便会允许咱们修改借用值:
文件名:`src/main.rs`
@@ -130,9 +125,9 @@ fn change(some_string: &mut String) {
```
首先,我们 `s` 改为了 `mut`。然后,我们在调用 `change` 函数处, `&mut s` 创建一个可变引用,并更新了函数签名,`some_string&mut String` 接受一个可变引用。这就清楚地表明,`change` 函数将改变其借用的值。
首先,我们修改 `s` `mut`。然后,在调用 `change` 函数处,我们以 `&mut s` 创建一个可变引用,并以 `some_string&mut String` 更新函数签名为接受一个可变引用。这使 `change` 函数将改变其借用的值非常清楚
可变引用有个很大的限制:如果咱们有到某个值的一个可变引用就不能对该值其他引用。下面这段试创建两个到 `s` 可变引用的代码,就会失败:
可变引用有个很大的限制:咱们有一个到某个值的可变引用时,咱们就不能对该值其他引用。下面这段试创建两个到 `s` 可变引用的代码失败:
文件名:`src/main.rs`
@@ -147,14 +142,14 @@ fn change(some_string: &mut String) {
```
下面是报错信息
下面是报错:
```console
$ cargo run
Compiling ownership_demo v0.1.0 (C:\tools\msys64\home\Lenny.Peng\rust-lang-zh_CN\projects\ownership_demo)
Compiling ref_demo v0.1.0 (/home/hector/rust-lang-zh_CN/projects/ref_demo)
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src\main.rs:5:14
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
@@ -165,23 +160,20 @@ error[E0499]: cannot borrow `s` as mutable more than once at a time
| -- first borrow later used here
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership_demo` (bin "ownership_demo") due to previous error
error: could not compile `ref_demo` (bin "ref_demo") due to 1 previous error
```
这个报错讲到,因为我们不能将 `s` 为可变引用,同一实际借用多次,因此这段代码无效。第一次可变借用`r1` 中,而必须持续到其在 `println!`使用为止,但在这可变引用的创建和使用之间,我们试图在 `r2`创建另一个借用`r1` 同一数据的可变引用
这个报表明这段代码无效,因为我们同一时间不能多次借用 `s` 为可变。第一次可变借用在 `r1` 处,并必须持续到其在 `println!` 中使用为止,但在这可变引用的创建和使用之间,我们试图在 `r2` 中创建另一个可变引用,借用与 `r1` 同一数据。
防止在同一时间,对同一数据进行多个可变引用的这种限制允许改变,但改变是在非常受控的方式进行。这是 Rust 新手比较头疼的问题,因为大多数语言都允许咱们随时改变。有着这限制的好处是Rust 可以在编译时防止数据竞赛。*数据竞data race* 类似于某种竞赛情形,会在下面这三种行为发生时出现:
阻止同时对同一数据的多重可变引用这一限制允许改变,但非常受控的方式进行。这是 Rust 新手比较头疼的问题,因为大多数语言都允许咱们随时修改数据。有着这限制的好处是 Rust 可以在编译时防止数据竞争。所谓 *数据竞data race*,类似于竞争条件,会在下面这三种行为发生时出现, is similar to a race condition
- 两个以上的指针同时访问同一数据;
- 至少有一个指针正用于写该数据;
- 没有用于同步访问该数据的机制。
- 两个以上的指针同时访问某同一数据;
数据竞争会导致未定义的行为当咱们试图在运行时追踪数据竞争时可能难于诊断和修复Rust 通过拒绝编译带有数据竞争的代码来防止这个问题!
- 至少有一个指针被用来写该数据;
- 没有同步访问该数据的机制。
数据竞赛会导致未定义的行为当咱们试图在运行时跟踪他们时会很难诊断和修复Rust 通过拒绝编译带有数据竞赛的代码,从而避免了这个问题!
与往常一样,我们可以使用花括号,创建一个新的作用域,从而允许多个可变引用,只要不是 *同时* 的多个:
与往常一样,我们可使用花括号创建新的作用域,允许多重可变引用,只要不是一些 *同时* 的多重引用即可:
```rust
@@ -189,13 +181,13 @@ error: could not compile `ownership_demo` (bin "ownership_demo") due to previous
{
let r1 = &mut s;
} // r1 在这里超出作用域,因此我们可以毫无问题地构造一个新的引用。
} // r1 在这里超出作用域,因此我们可以毫无问题地构造一个新的引用。
let r2 = &mut s;
```
Rust 对组合可变引用和不可变引用,也执行类似规则。下面这段代码会导致一个报错:
针对可变引用和不可变引用的组合Rust 会执行类似规则。下面这段代码会导致一个报错:
```rust
@@ -205,17 +197,17 @@ Rust 对组合可变引用和不可变引用,也执行类似的规则。下面
let r2 = &s;
let r3 = &mut s;
println! ("{}, {}{}", r1, r2, r3);
println! ("{r1}, {r2}{r3}");
```
下面是那个报错:
下面是报错:
```console
$ cargo run
Compiling ownership_demo v0.1.0 (C:\tools\msys64\home\Lenny.Peng\rust-lang-zh_CN\projects\ownership_demo)
Compiling ref_demo v0.1.0 (/home/hector/rust-lang-zh_CN/projects/ref_demo)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src\main.rs:6:14
--> src/main.rs:6:14
|
4 | let r1 = &s;
| -- immutable borrow occurs here
@@ -223,19 +215,19 @@ error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immuta
6 | let r3 = &mut s;
| ^^^^^^ mutable borrow occurs here
7 |
8 | println! ("{}, {}, {}", r1, r2, r3);
| -- immutable borrow later used here
8 | println! ("{r1}, {r2} 与 {r3}");
| -- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership_demo` (bin "ownership_demo") due to previous error
error: could not compile `ref_demo` (bin "ref_demo") due to 1 previous error
```
呼!我们 ** 不能在有着到某个值的不可变引用的同时,有着一个到这同一值的可变引用。
哎呀!当我们有着到同一值的不可变引用时,我们 ** 不能有可变引用。
某个不可变引用的使用者不期望值会突然在他们眼皮底下改变!不过,多不可变引用是允许的,因为仅读取数据的使用者,并无影响其他读取该数据使用者的能力
不可变引用的使用者不期望值会突然在他们眼皮底下改变!不过,多不可变引用是允许的,因为仅读取数据的使用者没有能力影响其他使用者对数据的读取
请注意,引用的作用域从该引用被引入的地方开始,持续到该引用最后一次使用为止。例如,下面这段代码可以编译,因为那个不可变引用最后一次使用(`println!`),发生在那个可变引用被引入前:
请注意,引用的作用域从其被引入之处开始,持续到该引用最后一次使用为止。例如,下面这段代码编译,因为不可变引用最后使用是在 `println!` 中,在可变引用被引入前:
```rust
@@ -243,31 +235,28 @@ error: could not compile `ownership_demo` (bin "ownership_demo") due to previous
let r1 = &s; // 没有问题
let r2 = &s; // 没有问题
println! ("r1 与 r2: {}, {}", r1, r2);
// 变量 r1 与 r2 在此处之后将不会使用
println! ("{r1} {r2}");
// 变量 r1 与 r2 在此处之后将不会使用
let r3 = &mut s; // 这就没问题
println! ("r3: {}", r3);
let r3 = &mut s; // 没问题
println! ("{r3}");
```
不可变引用 `r1``r2` 的作用域在他们最后一次被使用的 `println!` 之后,可变引用 `r3` 被创建之前结束。这些作用域不重叠,因此这段代码会被放行:编译器可以区分出在作用域结束前的某个点,该引用不再被使用。
不可变引用 `r1``r2` 的作用域在他们最后被使用`println!` 后结束,这在可变引用 `r3` 被创建。这些作用域不重叠,因此这段代码会被放行(允许):编译器可以区分出引用在作用域结束前的某个点不再被使用。
> **译注**:由于引用属于大小已知、固定不变的类型,因此他们是保存在栈上的,带有 `Copy` 特质,故上面的代码中,在 `let r3 = &mut s;` 语句之前,可以无限次使用 `r1` 和 `r2` 这两个不可变引用,他们不会被迁移到 `println!` 宏及其他函数中。
尽管借用方面的报错有时会令人沮丧,但请记住,这是 Rust 编译器在早期(编译时而不是运行时)指出某个潜在错误,并准确地告诉咱们问题所在。这样,咱们不必再追踪,为什么咱们的数据和咱们设想的不一样了
尽管借用的报错有时会令人沮丧,但请记住,这是 Rust 编译器在早期(编译时而不是运行时)指出某个潜在 bug并向咱们准确展示问题所在。然后,咱们不必排查为何咱们的数据不是咱们认为的那样
## 悬空引用
**Dangling References**
在有着指针的语言中,通过释放一些内存同时保留指向该内存的指针,很容易错误地创建 *悬空指针dangling pointer* -- 引用内存中可能已经给了另一指针/变量的某个位置的指针。相比之下,在 Rust 中,编译器保证引用将绝不是悬空引用:当咱们有着到某一数据的引用时,编译器会确保该数据不会在指向该数据的引用超出作用域前超出作用域。
在带有指针的语言中,就很容易错误地创建出 *悬空指针dangling pointer*,即在保留了指向某处内存指针的同时,释放了该处内存,从而造成引用了内存中,可能已经给了其他代码的某个位置的指针。相比之下,在 Rust 中,编译器会保证引用,永远不会成为悬空引用:如果咱们有个到某数据的引用,编译器会确保该数据,不会在指向该数据引用超出作用域之前,超出作用域。
我们来尝试创建一个悬挂引用,看看 Rust 如何通过编译时报错,来防止他们:
我们来尝试创建一个悬挂引用,看看 Rust 如何通过编译时报错来防止他们:
文件名:`src/main.rs`
@@ -284,29 +273,34 @@ fn dangle() -> &String {
}
```
下面是那个报错:
下面是报错:
```console
$ cargo run
Compiling ownership_demo v0.1.0 (C:\tools\msys64\home\Lenny.Peng\rust-lang-zh_CN\projects\ownership_demo)
Compiling ref_demo v0.1.0 (/home/hector/rust-lang-zh_CN/projects/ref_demo)
error[E0106]: missing lifetime specifier
--> src\main.rs:5:16
--> 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
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
5 | fn dangle() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
|
For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership_demo` (bin "ownership_demo") due to previous error
error: could not compile `ref_demo` (bin "ref_demo") due to 1 previous error
```
报错信息涉及我们尚未讲到的一项特性:生命周期。我们将在第 10 章详细讨论生命周期。但是,如果咱们不考虑生命周期的部分,这消息确实包含了,为什么这段代码会出现问题的关键所在
报错信息涉及我们尚未讲到的一项特性:生命周期。我们将在第 10 章详细讨论生命周期。但是,咱们不考虑生命周期的部分,这消息确实包含了为何这段代码是个问题的关键:
```console
@@ -315,17 +309,17 @@ for it to be borrowed from
```
我们来仔细看看,咱们 `dangle` 代码的每个阶段到底发生了什么:
我们来仔细看看,咱们 `dangle` 代码的每个阶段到底发生了什么:
文件名:`src/main.rs`
```rust
fn dangle() -> &String { // dangle 返回的是个到 String 的引用
fn dangle() -> &String { // dangle 返回个到 String 的引用
let s = String::from("hello"); // s 是个新的 String
&s // 们返回一个指向那个 Strings 的引用
} // 这里s 超出作用域,进而被丢弃。他的内存没了。
&s // 们返回一个指向 Strings 的引用
} // 这里s 超出作用域而被丢弃。因此他的内存没了。
// 危险所在!
```