指针类型
指针类型
Rust的借用
定义:获取变量的引用,称之为借用(borrowing)
虽然Rust主要通过所有权管理,不像C那样经常存在多个指针引用同一地址的问题
但是Rust也有类似的东西。详见 Rust 的 “引用与借用”
又分:
- 不可变引用
- 可变引用
为什么需要
其实不使用这个特性,似乎没有功能上的损失。所有权系统都能做到
但写函数的时候,总要把参数传进来再返回回去,写起来非常臃肿。所以也可以看作是一种语法糖
如何保证内存安全
我们之前知道了Rust的所有权机制,是如何保证内存安全的。
但现在又引入引用和借用机制,难道不会破坏之前设计好的内存安全和数据竞争吗?还是通过什么机制来保证安全和数据竞争?
数据竞争
原因
数据竞争可由以下行为造成
- 两个或更多的指针同时访问同一数据
- 至少有一个指针被用来写入数据
- 没有同步数据访问的机制
策略
为此添加以下规则
- 可变引用同时只能存在一个
- 可变引用与不可变引用不能同时存在
或浓缩一下
- 同一时刻,你只能拥有要么一个可变引用,要么任意多个不可变引用
原理
原理上有点像读写锁,避免数据竞争。
读写锁不能同时lock,且只能有一个写锁在lock
技巧
多用 {}
来限制引用的作用域,会灵活些
理解
或者可以用一些比喻来理解。不用 “引用” 这个其他语言的词,而是用回 “借用”。
正在借用不可变引用的用户,肯定不希望他借用的东西,被另外一个人莫名其妙改变了。
从这一点来看,其实 “借用” 与单个可变引用/不可变引用 (看作是所有者不允许被借书的人在上面画东西) 更贴切些。与多个不可变引用没那么贴切,毕竟不太能同时把东西借用给多个人
内存问题,悬垂引用问题
检查器的一些个人猜测
- 检查所有者变更后,旧所有者是否还会被使用那样。也可以检查是所有者会被借用者还回数据前,是否会被调用
- 在简单场景上,如调用函数。可以看作是还是使用所有权机制的一种语法糖
在 Rust 中编译器可以确保引用永远也不会变成悬垂状态
让我们尝试创建一个悬垂引用,Rust 会抛出一个编译时错误
下面是一个例子
(顺便注释了 dangle
代码的每一步到底发生了什么)
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle 返回一个字符串的引用
let s = String::from("hello"); // s 是一个新字符串
&s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
// 危险!
错误信息引用了一个我们还未介绍的功能:生命周期(lifetimes)。不过,即使你不理解生命周期,也可以通过错误信息知道这段代码错误的关键信息:
this function's return type contains a borrowed value, but there is no value for it to be borrowed from.
该函数返回了一个借用的值,但是已经找不到它所借用值的来源
其中一个很好的解决方法是直接返回 String
:
fn no_dangle() -> String {
let s = String::from("hello");
s
}
这样就没有任何错误了,最终 String
的 所有权被转移给外面的调用者。
其他 - NLL
一些编译器优化杂谈
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、r3作用域在这里结束
// 新编译器中,r3作用域在这里结束
在老版本的编译器中(Rust 1.31 前),将会报错,因为 r1
和 r2
的作用域在花括号 }
处结束,那么 r3
的借用就会触发 无法同时借用可变和不可变 的规则。
但是在新的编译器中,该代码将顺利通过,因为 引用作用域的结束位置从花括号变成最后一次使用的位置,因此 r1
借用和 r2
借用在 println!
后,就结束了,此时 r3
可以顺利借用到可变引用。
对于这种编译器优化行为,Rust 专门起了一个名字 —— Non-Lexical Lifetimes(NLL),专门用于找到某个引用在作用域(}
)结束前就不再被使用的代码位置。