2.3.2 引用与借用
1 引用与借用
Rust 通过 借用(Borrowing)
这个概念来达成上述的目的,获取变量的引用,称之为借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来,当使用完毕后,也必须要物归原主。
1.1 引用与解引用
常规引用是一个指针类型,指向了对象存储的内存地址。在下面代码中,我们创建一个
i32
值的引用 y
,然后使用解引用运算符来解出
y
所使用的值:
1 |
|
变量 x
存放了一个 i32
值
5
。y
是 x
的一个引用。可以断言
x
等于 5
。然而,如果希望对 y
的值做出断言,必须使用 *y
来解出引用所指向的值(也就是
解引用)。一旦解引用了 y
,就可以访问
y
所指向的整型值并可以与 5
做比较。
不允许比较整数与引用,因为它们是不同的类型。必须使用解引用运算符解出引用所指向的值。
1.2 不可变引用
下面的代码,我们用 s1
的引用作为参数传递给
calculate_length
函数,而不是把 s1
的所有权转移给该函数:
1 |
|
能注意到两点:
- 无需像上章一样:先通过函数参数传入所有权,然后再通过函数返回来传出所有权,代码更加简洁
calculate_length
的参数s
类型从String
变为&String
这里,&
符号即是引用,它们允许你使用值,但是不获取所有权,如图所示:
通过 &s1
语法,我们创建了一个 指向
s1
的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域后,其指向的值也不会被丢弃。
同理,函数 calculate_length
使用 &
来表明参数 s
的类型是一个引用:
1 |
|
如果尝试修改借用的变量呢?
1 |
|
很不幸,妹子你没抱到,哦口误,你修改错了。正如变量默认不可变一样,引用指向的值默认也是不可变的。
1.3 可变引用
只需要一个小调整,即可修复上面代码的错误:
1 |
|
首先,声明 s
是可变类型,其次创建一个可变的引用
&mut s
和接受可变引用参数
some_string: &mut String
的函数。
1.3.1 可变引用同时只能存在一个
不过可变引用并不是随心所欲、想用就用的,它有一个很大的限制:同一作用域,特定数据只能有一个可变引用:
1 |
|
以上代码会报错。
这段代码出错的原因在于,第一个可变借用 r1
必须要持续到最后一次使用的位置 println!
,在 r1
创建和最后一次使用之间,我们又尝试创建第二个可变借用
r2
。
对于新手来说,这个特性绝对是一大拦路虎,也是新人们谈之色变的编译器
borrow checker
特性之一,不过各行各业都一样,限制往往是出于安全的考虑,Rust
也一样。
这种限制的好处就是使 Rust 在编译期就避免数据竞争,数据竞争可由以下行为造成:
- 两个或更多的指针同时访问同一数据
- 至少有一个指针被用来写入数据
- 没有同步数据访问的机制
数据竞争会导致未定义行为,这种行为很可能超出我们的预期,难以在运行时追踪,并且难以诊断和修复。而 Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码!
很多时候,大括号可以帮我们解决一些编译不通过的问题,通过手动限制变量的作用域:
1 |
|
1.3.2 可变引用与不可变引用不能同时存在
下面的代码会导致一个错误:
1 |
|
错误如下:
其实这个也很好理解,正在借用不可变引用的用户,肯定不希望他借用的东西,被另外一个人莫名其妙改变了。多个不可变借用被允许是因为没有人会去试图修改数据,每个人都只读这一份数据而不做修改,因此不用担心数据被污染。
注意,引用的作用域
s
从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号}
Rust 的编译器一直在优化,早期的时候,引用的作用域跟变量作用域是一致的,这对日常使用带来了很大的困扰,你必须非常小心的去安排可变、不可变变量的借用,免得无法通过编译,例如以下代码:
1 |
|
在老版本的编译器中(Rust 1.31 前),将会报错,因为 r1
和
r2
的作用域在花括号 }
处结束,那么
r3
的借用就会触发 无法同时借用可变和不可变
的规则。
但是在新的编译器中,该代码将顺利通过,因为
引用作用域的结束位置从花括号变成最后一次使用的位置,因此
r1
借用和 r2
借用在 println!
后,就结束了,此时 r3
可以顺利借用到可变引用。
所有权很强大,避免了内存的不安全性,但是也带来了一个新麻烦:总是把一个值传来传去来使用它。传入一个函数,很可能还要从该函数传出去,结果就是语言表达变得非常啰嗦,幸运的是,Rust 提供了新功能解决这个问题。
1.3.3 NLL
对于这种编译器优化行为,Rust 专门起了一个名字 —— Non-Lexical
Lifetimes(NLL),专门用于找到某个引用在作用域(
}
)结束前就不再被使用的代码位置。
1.4 悬垂引用(Dangling References)
悬垂引用也叫做悬指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。在 Rust 中编译器可以确保引用永远也不会变成垂状态:当你获取数据的引用后,编译器可以确保数据不会在引用结束前被释放,要想释放数据,必须先停止其引用的使用。
让我们尝试创建一个悬引用,Rust 会抛出一个编译时错误:
1 |
|
这里是错误:
1 |
|
错误信息引用了一个我们还未介绍的功能:生命周期(lifetimes)。不过,即使你不理解生命周期,也可以通过错误信息知道这段代码错误的关键信息:
1 |
|
仔细看看 dangle
代码的每一步到底发生了什么:
1 |
|
因为 s
是在 dangle
函数内创建的,当
dangle
的代码执行完毕后,s
将被释放,但是此时我们又尝试去返回它的引用。这意味着这个引用会指向一个无效的
String
,这可不对!
其中一个很好的解决方法是直接返回 String
:
1 |
|
这样就没有任何错误了,最终 String
的
所有权被转移给外面的调用者。
1.4.1 借用规则总结
总的来说,借用规则如下:
- 同一时刻,你只能拥有要么一个可变引用,要么任意多个不可变引用
- 引用必须总是有效的