likes
comments
collection
share

写给前端开发者的 Rust 入门指南 Part 3 - 借用

作者站长头像
站长
· 阅读数 17

学习一门底层语言,犹如修炼内功,帮助我们以更底层的视角看待编程,写出健壮安全的代码。修炼 Rust 绝非一朝一夕,本系列文章旨在从 JavaScript 开发者的角度,对比和厘清 Rust 中那些有趣的,难啃的,或独有的概念,帮助同胞们迈出学习 Rust 的第一步。

完整文章传送门:

写给前端开发者的 Rust 入门指南 Part 1 - 内存

写给前端开发者的 Rust 入门指南 Part 2 - 所有权

写给前端开发者的 Rust 入门指南 Part 3 - 借用 📍当前位置

写给前端开发者的 Rust 入门指南 Part 4 - 生命周期

前两篇文章,我们从内存谈起,进而隆重介绍了所有权机制,本篇登场的是所有权的孪生兄弟 -- 借用(borrowing)。借用是所有权衍生出的最重要的机制,它们密切相关,又泾渭分明。理解了所有权和借用,就推开了 Rust 世界的大门。

从引用到借用

从我们熟悉的 JavaScript 说起:

function pop(data) {
  data.pop()
}

let v = [1,2,3];
pop(v);

let vlen = v.length; // 取决于 pop 函数对 v 做了什么修改

很简单的一段代码,可能在我们的程序中随处可见。请把注意力放在 pop 调用时,传入的数组 v。由于 v 是引用类型数据,在函数传参时,传的是v 的引用,这是 JS 的默认行为。传递引用意味着 pop 函数对 data 的任何修改,都会影响变量 v,因为 data 和 v 指向同一个内存地址。可以想像,在一个大型 JavaScript 应用中,无数对象的引用散落在代码的各个角落,彼此之间交织缠绕,复杂得像一张网,最终依靠垃圾回收(GC)兜底。

与 JS 不同,在所有权机制的保护下,Rust 函数传参时默认传递的是值的所有权,也可以理解为,传递的是值本身:

fn logData(data: Vec<i32>)  {
    println!("data -> {:?}", data);
}

fn main() {
    let v = vec![1,2,3]; // 动态数组的所有权属于 v

    logData(v); // v 把数据所有权转移给给参数 data,v 引用失效
    //-------- logData 运行结束,data 内存释放,动态数组失效
    
    println!("{:?}",v); // error!访问了一个无效的引用
}

logData 调用时,入参接收到的是动态数组的所有权,于是变量 v 在 logData 调用后就无法被访问了。这和 JS 的机制完全不同,在 Rust 看来,同一个内存地址被多个引用访问/修改是极其危险的行为,所以引用传递不能是默认行为,所有的引用都必须显式地创建和传递,清清楚楚明明白白:

fn logData(data: &Vec<i32>) { // 接受的参数是引用类型 - &Vec<i32>,
    println!("打印成功:{:?}",data);
}

fn main() {
    let v = vec![1,2,3];
    logData(&v); // 传入 v 的只读引用,不转移所有权
    println!("{:?}",v); // 此时 v 依然可以访问
}

以上代码有两处改变:

  1. logData 定义时参数 data 类型变成 &Vec<i32>
  2. logData 调用时传入的是 &v

& 是 Rust 中的引用符号。在值的前面,它表示值的引用,如 &v 表示 v 的引用;放在类型前面,表示引用的类型,如 &Vec<i32> 表示引用的类型为Vec<i32>。 logData 接收 &v,并不接管 v 的所有权,所以调用结束后,变量 v 依然可以被访问。可以看到,要实现和 JS 一样的参数引用传递,我们要给变量(数据)显式加上 &

回忆一下,Rust 的「所有权机制」要求:每一个值都有唯一的拥有者。所以在 Rust 中,值的引用,实际上是从值的拥有者那里来的。变量 v 至始至终拥有堆上动态数组的所有权,它只是「出借」了数据的引用,所有者销毁时,其引用也不能继续存在。在 Rust 中,将创建值的引用的行为,称作借用(borrowing)。

说到底,借用就是引用。与其他语言不同的是,在 Rust 所有权机制的影响下,引用行为需要遵循特定的规则(下文见分晓)。「借用」作为 Rust 独有的编程概念,有必要仔细辨析一下:Rust 之「借用」既可以作动词,表示创建引用的动作(&v 借用了 v 的数据);也能够作名词,表示引用本身(&v 是 v 的借用)。大多数情况下「借用」和「引用」二词可以相互替换。

认识借用

在 Rust 语境下,所有的引用都是从所有者那里过来的,借用值受到出借者的限制,出借者也会受到借用值的影响。本节从最简单的场景出发,试图阐述「借用」机制最核心的特性。

借用的内存分布

& 符号创建一个整数类型值的借用/引用:

let a: i32 = 88;
let b1: &i32 = &a;

println!("变量 a 绑定的值是: {:?}", a); // 88
println!("变量 a 的内存地址是: {:p}", &a); // 0x16f9bead4
println!("变量 b1 的内存地址是: {:p}", &b1); // 0x16f9bead8

以上的代码中,变量 a 拥有整数 88 的所有权。&a 创建了 a 的引用,赋值给 b1,接着分别打印:变量 a 的值、变量 a 的内存地址、变量 b1 的内存地址。请注意,打印内存地址时,所用的占位符号是 {:p},不同于{:?}。p 代表 Pointer,即指针,所以该占位符号只能打印引用(指针),打印的结果是指针所指向的内存地址,不是指针本身的地址

就本例而言,想要知道变量 a 的内存地址(也就是整数88所在的内存地址),就要打印变量 a 的引用 -- 即 &a 的值。同理,想要知道 b1(a 的引用)变量本身的地址,需要打印 b1 的引用 -- 即 &b1(或&&a) 的值。从打印结果可以看出,变量 b1 作为变量 a 的引用,储存在一个独立的内存地址,和变量 a 的地址是不同的

写给前端开发者的 Rust 入门指南 Part 3 - 借用

PS:打印出的内存地址在你我的电脑上是不同的,如果相同也太有缘份了吧!

思考一个有意思的问题:在 JavaScript 中,我们怎么创建一个基础类型值的引用,比如上例中的整数 88 呢?答案是,把值包装到对象中:

let numObj = {
  value: 88
}

let b1 = numObj // 创建引用
let b2 = numObj // 再来一个引用

console.log(numObj.value, b1.value, b2.value) // 88 88 88

JavaScript 没有显式创建引用的能力,但我们可以把基础类型值封装到引用类型值内部,间接创建基础类型值的共享引用。是不是很眼熟?是的,Vue3 中的响应式 ref 就是基于这样的封装,才能让基础类型值拥有「响应式」:

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

React 中的 useRef 也是同样的道理:

const count = useRef(0);

useEffect(() => {
  count.current = count.current + 1;
});

这就是学习底层语言收获的不同视角。作为 JavaScript 开发者,平常直接使用 Vue,React 等框架的种种「魔法」,也许从来没有认真思考过「引用」是如何创建的?存在哪里?有什么权限?什么时候销毁?虽然这类思考说不上多有价值,但获得一个不同的视角,总归是感到有趣的吧。

多个(只读)借用

一个值可以有多个借用,它们都是只读的,不能修改值:

fn main() {
    let v = vec![1,2,3];

    let b1 = &v;
    let b2 = &v;
    let b3 = &v;
    
    // 动态数组所有者 v 的地址
    println!("v 的地址: {:p}", &v);
        
    // 打印值的地址
    println!("b1指向的地址: {:p} | b2指向的地址: {:p} | b3指向的地址:{:p}", b1, b2 ,b3);

    // 打印指针自身的地址
    println!("b1的地址: {:p} | b2的地址: {:p} | b3的地址:{:p}", &b1, &b2 ,&b3);
    
/* 
打印结果:

v 的地址: 0x16d71aa50

b1指向的地址: 0x16d71aa50 | b2指向的地址: 0x16d71aa50 | b3指向的地址:0x16d71aa50

b1的地址: 0x16d71aa68 | b2的地址: 0x16d71aa70 | b3的地址:0x16d71aa78
*/
}

从打印结果可以看出,b1,b2,b3 全部指向「出借者 v」的地址:0x16d71aa50;而这三个指针变量本身储存在不同的地址:0x16d71aa680x16d71aa700x16d71aa78。可见,创建一个引用,就是创建了一个值,储存在独立的内存地址,却指向同一个的地方:

写给前端开发者的 Rust 入门指南 Part 3 - 借用

借用的行为,实际上是把「引用」赋值给了一个变量let b1 = &v),进而创建了一个储存在独立地址的指针变量。在所有权篇说过「Copy 特性」,其实「引用」也具备 Copy 特性,所以在赋值传递、函数传参数的时候,会按位复制一份新的指针。

「借用套娃」

Rust 支持「引用一个引用」,或者说「借用一个借用」:

fn main() {
    let v: Vec<i32> = vec![1,2,3];

    let b1: &Vec<i32> = &v;
    let b2: &&Vec<i32> = &b1;
    let b3: &&&Vec<i32> = &b2;
}

套娃预警:b1 借用了 v,b2 借用了 b1,b3 借用了 b2。从代码功能上来说,这段代码和上例中的借用完全相同,只是内存分布有所不同:

写给前端开发者的 Rust 入门指南 Part 3 - 借用

从图上可以很直观看出,引用可以顺着借用链条一路找到最终的数据。代码上标注了 b1/b2/b3 的类型,存在多个 & 符号叠加的情况,理论上可以无限叠加下去,&&&& 就像一个个脚印🦶🦶🦶🦶。「冤有头债有主」,Rust 总会循着脚印找到债主(出借者v)。

可变借用

上述所有例子中出现的借用,称之为共享借用(Shared Reference),也可以叫只读借用,或者不可变借用。「共享」强调可以有多个引用,「只读」强调这些引用只有只读权限,我认为对于不可变的引用来说,「只读」特性的重要性大于「共享」,所以后文仅采用「只读借用」的说法。

如果我们希望能够修改借用过来的值,可以执行「可变借用」:

fn main() {
    let mut v = 99; // 声明可变变量 v
    
    let m1 = &mut v; // 可变借用
    *m1 += 1; // 解引用符号 *,解开引用取到值,执行 +=1
    
    println!("{:?}",v); // 100
}

首先,Rust 中所有的变量,默认是不可改变的(immutable)。如果想要修改,需要声明的时候加上 mut 关键字,mut 意思是 mutable(可变的)。相应地,如果需要可变借用,只能从可变的变量那里借let mut v = 99)。借用的时候,& 符号后面需要加上 mut 关键字(let m1 = &mut v)。

变量 m1 是 v 的「可变借用」,意味着 m1 拥有修改整数 99 的权限。注意到 *m1 += 1 的星号 * 了么?这个叫「解引用符号」,在一个引用前面加 * 意思是:「麻烦帮我取到这个引用所指向的内存地址的值,谢谢!」。一个引用的值是这个引用所指向的内存地址(比如0x16d71aa68),而这个内存地址上储存的值,才是引用真正指向的值。变量 m1 是一个引用,引用不能拿来加减乘除,所以需要通过 * 符号取到 m1 引用的真值 99,这个行为叫做解引用(dereference)。

隐式/自动解引用

引用/借用在程序中随处可见,所以解引用也几乎无处不在。为了避免代码到处都是 * 符号,Rust 在某些特定的场景会自动帮我们解开引用的值:

fn main() {
    let mut v = vec![1,2,3];
    
    let m1 = &mut v;   
    m1.push(4); // = (*m1).push(4)
    
    println!("{:?}",v); // [1,2,3,4]
}

上例中,变量 m1 作为一个引用类型的值,但在执行修改动作的时候(m1.push(4)),并没有通过 * 符号对 m1 解引用,而是直接调用 push 方法,m1 只是一个安安分分的引用,怎么可能会有 push 方法呢?。实际上,是 Rust 自动为我们解开了引用,找到了 m1 背后的动态数组,数组上有 push 方法。我们写m1.push(4) 相当于执行了 (*m1).push(4)

另外,.操作符也会发生自动解引用:

struct Person {
  name: String,
  age: i32
}

fn main() {
    let tom = Person {name: String::from("Tom"), age: 35 };
    let tom_ref = &tom;
    
    // tom_ref.name 相当于 (*tom_ref).name
    println!("name: {:?}", tom_ref.name); 
    
    // tom_ref.age 相当于 (*tom_ref).age
    println!("age: {:?}", tom_ref.age); 
}

变量 tom_ref 作为结构体 Person 实例的引用,在 println! 调用时,可以直接通过 . 访问到结构体的上的属性(name,age),tom_ref 只是一个安安分分的引用,怎么可能有 name 和 age 呢?这也是 Rust 隐式地为我们解开了引用,因为结构体取值是高频动作,如果每次通过引用取值都要写出 (*tom_ref).name 这样的代码,太不优雅。

还有一个很常见的隐式解引用的场景 -- 比较大小:

fn main() {
    let a = 100;
    let b = 200;

    let a1 = &a;
    let b1 = &b;
    
    // 直接用引用比较大小。打印:a1 < b1: true
    println!("a1 < b1: {:?}", a1 < b1);
}

上例中的代码足够简单,引用 a1 和引用 b1 可以直接比较大小。a1 和 b1 只是安安分分的引用,怎么能比较大小呢?因为 Rust 在比较操作这种常见的情况,为我们自动(或者说隐式)地解引用了。

另外值得注意的是,对堆上数据执行解引用操作会转移数据的所有权,:

fn main() {
    let mut v = vec![1,2,3]; 
    
    let m1 = &v;
    let m2 = *m1; // 对 m1 接引用,会把数据所有权转移给 m2,导致 m1 引用失效
}

这段代码不会编译通过,报错:cannot move out of m1 which is behind a shared reference。简单来说,就是不能转移引用的所有权。解引用操作 *m1 会把 m1 的所有权转移给 m2,由于 m1 引用了 v,也间接导致 v 的所有权被转移。假如转移成功,m1 指针指向的还是 v 的原内存地址,而 v 在转移之后就失效了,引用出错。为了避免大做特错,所以 Rust 编译器及时出手阻止。

当然,Rust 隐式解引用的场景不止以上这些,好在并不需要死记硬背。毕竟这东西是自动的,大多数情况下不需要我们关心,只要按照正常脑回路写代码就好,真到需要手动处理的情况,强大的 Rust 编译器也会提醒我们的。

借用的规则

不知道你有没看过《猎人》或者《JOJO 的奇妙冒险》这类「智斗」漫画。其中角色的特殊能力和战斗体系,都依托于各种各样的「规则」。作品精彩之处就在于角色对「规则」的熟练掌握和巧妙运用。甚至可以说,没有规则,这些作品就失去了灵魂。编程语言也一样,都要遵循各种各样的规则,开发者只有熟悉规则,活用规则,才能「大杀四方」。

借用的生命周期

总是说 Rust 是一门安全的语言,但引用似乎也是散落各处,遍地开花?这和 JS 有什么差别呢?如果你有这个疑惑,请在心中默念所有权咒语:「一个值只有一个所有者,所有者离开作用域,值被释放」。我们可以从所有者那里借来值的引用,但所有权还是牢牢掌握在所有者的手上。借用机制是对所有权机制的尊重,而不是违背。Rust 中的引用必须遵循以下规则:

出借者(所有者)的生命周期,必须大于其引用(借用者)

换句话说:

一个引用,不能活得比它引用的值还长

这从逻辑上很好理解。一个值(所有者)已经被清除释放,如果它的引用还活着,就会指向一个空值或者错误的值(原内存被其他值占用),从而形成迷途指针(Dangling Pointer)。迷途指针也叫悬垂指针,野指针...但我喜欢迷途指针这个说法,因为听起来很形象,就像引用迷路了,找不到正确的值了。Rust 不允许这种情况发生,所以代码里出现任何违反上诉规则的情况,编译器就会无情报错。

fn main() {
   let v;
   
   // --- 花括号形成独立的作用域 ---
   {	   
      let x = vec![1,2,3];
      v = &x; // v 是 x 的引用  
   } 
   // --- 花括号作用域结束 ---
   // 变量 x 被销毁,v 变成了迷途指针,出错 !

   println!("v -> {:?}", v); // 访问迷途指针,出错!!
}

在 Rust 代码里,花括号 {...} 包围的空间会形成独立的作用域,就像函数的作用域一样。在 JavaScript 里花括号也能形成「块级作用域」。我们知道 Rust 作用域里的变量,在离开作用域的时候,会被销毁。也就是说变量 x 在离开花括号作用域的时候就没了,x 引用又赋值给了花括号外边的 v,这导致 v 就变成了迷途指针,指向了未知的黑暗。编译器会报如下错误:

error[E0597]: `x` does not live long enough
  --> src/any.rs:10:11
   |
10 |       v = &x;
   |           ^^ borrowed value does not live long enough
11 |    }
   |    - `x` dropped here while still borrowed
12 | 
13 |    println!("v -> {:?}", v)
   |                          - borrow later used here

For more information about this error, try `rustc --explain E0597`.

Rust 的报错信息非常精致,有划线有标注,生怕你看不懂。我们来仔细看看:

  • 首先 x does not live long enough 开篇名义:变量 x 存活的时间不够久
  • 接着,在 &x 的下方有个小标注: 说 borrowed value does not live long enough。意思是,出借者 x 都活不久了,借用 &x 也不能苟活
  • 在花括号结束时(}),标注了 - x dropped here while still borrowed。意思是,变量 x 在这里就被释放啦,可是引用 v 依然指向它(迷途指针);
  • println! 中的 v 底下标注了 - borrow later used here。意思是,v 已经迷路了,却又在这里用了,不行🙅;
  • 最后,还是不知道怎么回事?请输入命令 rustc --explain E0597,我给你展开说说。

好贴心呀,仿佛在看一个学霸的笔记,标注清晰,循循善诱。这也是 Rust 的一大优点:完备的报错信息。Rust 编译器会 review 你的代码,对于质量不过关的地方不但给你标记出来,还会说清楚为什么你不清楚(😂😂😂)。基本上只要过了 Rust 编译器这一关,生产环境的代码就会非常强健安全,轻易不出 bug。

错误信息里面反复出现一个值「活得够不够长」的说法,我们不妨来标注一下值的寿命:

写给前端开发者的 Rust 入门指南 Part 3 - 借用

红色部分是「数据所有者x」的生命周期,蓝色部分是「借用者v」的生命周期。一目了然,借用者的生命周期包含了所有者生命周期,也就是说,引用(借用者)比它所引用值(出借者)活得长。

回味一下前文提及的借用规则:

一个引用,不能活得比所引用的值还长

所以很明显,这段代码是错误的。

可见 Rust 编译器会维护和比较每个值的生命周期长短,如果出现借用者(引用)的生命周期大于出借者(所有者)生命周期的情况,就会编译失败。

借用的可变性互斥

我们知道如何创建一个值的「只读借用」和「不可变借用」。请看以下代码:

fn main() {
    let mut v = vec![1,2,3];

    let b1 = &v; // 只读借用
    let m1  = &mut v; // 可变借用
    m1.push(4); // 修改值
    
    println!("v的值是{:?}",b1); // 打印只读借用
}

以上代码对变量 v 做了只读借用(b1)和不可变借用(m1)。然后试图通过 m1 修改动态数组的值,最后再打印只读借用 b1 的值。这段代码编译时会无情报错:

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/any.rs:5:15
  |
4 |     let b1 = &v;
  |              -- immutable borrow occurs here
5 |     let m1  = &mut v;
  |               ^^^^^^ mutable borrow occurs here
...
8 |     println!("v的值是{:?}",b1);
  |                            -- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.

错误信息明确告诉我们:不能创建 v 的可变借用,因为 v 已经存在只读借用了。我们之前反复说过,Rust 认为「存在多个引用可以访问/修改同一个内存地址」是非常危险的行为。因此,一个值不能同时存在只读借用和可变借用。说得严谨一些:

一个作用域内,只能出现值的一个「可变借用」,或多个「只读借用」。这两者是互斥的。

再换一种说法:

一个作用域内,如果对一个值做了只读借用,就不能再对它做可变借用了。反之,一个值有了可变借用,就不能再出现它的只读借用了

翻来覆去说了这么多遍,归根结底一句话:「能读不能写,能写不能读」。这其实就是我们常说的「读写锁」模型:同一时间,对一个资源,可以有多个「读者」读取它的数据,但只能有一个「写者」修改它的数据,并且读的时候不能写,写的时候禁止读。读取数据是很安全的行为,所以 Rust 允许同时存在多个只读引用,值就在那里,你需要就来读取,多少人同时来取都可以。但修改数据是很危险的行为,如果大家都来修改,你一笔我一划,那最后听谁的?如果写的时候同时又在读,读取的数据会长什么样?

隐式借用

有些情况,借用是隐式的,不那么容易看出来:

fn main() {
    let mut v = vec![1,2,3];
    
    let b1 = &v;
    v.push(4); // 自动创建了 v 的可变借用,error!
    
    println!("v的值是{:?}",b1);
}

报错信息一目了然👇:

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/any.rs:6:5
  |
4 |     let b1 = &v;
  |              -- immutable borrow occurs here
5 |     
6 |     v.push(4);
  |     ^^^^^^^^^ mutable borrow occurs here
7 | 
8 |     println!("v的值是{:?}",b1);
  |                            -- immutable borrow later used here

这段代码也不能编译通过。变量 v 在有了只读借用 b1 的情况下,v.push(4) 却修改了动态数组的值,违反了「只读借用和可变借用互斥」的规则。虽然我们从头到尾没有对 v 做可变借用,但 v.push(4) 替我们做了隐式的可变借用,相当于 (&mut v).push(4)。隐式借用是为了编码方便,就像上文说的自动解引用。虽说强扭的瓜不甜,但你不借,Rust 有时候可能会帮你借(都是为了你好🤭)。

类似的👇:

fn main() {
    let mut v = vec![1,2,3];
    let m1 = &mut v; // 第一个可变借用
    v.push(4); // 第二个可变借用 error!!
    println!("v的值是{:?}",m1);
}

以上代码违反了「同时只能存在一个可变借用」的原则,报错信息一目了然:

error[E0499]: cannot borrow `v` as mutable more than once at a time 变量 v 不能同时有两个可变借用哦!
 --> src/any.rs:4:5
  |
3 |     let m1 = &mut v;
  |              ------ first mutable borrow occurs here 第一个可变借用
4 |     v.push(4);
  |     ^^^^^^^^^ second mutable borrow occurs here 第二个可变借用
5 |     println!("v的值是{:?}",m1);
  |                            -- first borrow later used here

For more information about this error, try `rustc --explain E0499`.

Non-Lexical Lifetimes(NLL)

这个例子可能会突破刚建立起来的认知👇:

fn main() {
    let mut v = 99;

    // 第一个可变借用
    let m1 = &mut v;
    *m1 += 1;
    
    // 第二个可变借用
    let m2 = &mut v;
    *m2 += 1;
    
    // 再来一个只读借用
    let b1 = &v;
    println!("v: {:?}", b1); // 成功打印 101
}

以上代码竟然可以编译通过!在 main 函数作用域中,同时存在两个可变借用:m1 和 m2。说好的「一个作用域内只能同时存在一个可变借用呢!!??」有多个可变借用就算了,竟然还出现了只读借用 b1,说好的「一个作用域内,只读借用和可变借用互斥呢!!??」。这段代码恨不得把能违反的借用规则全违反了,但却编译成功了?!!

先冷静一下。其实这种情况叫做 Non-Lexical Lifetimes(NLL),翻译过来是「非词法作用域生命周期」。我们知道,变量的生命周期受所其所在的作用域限制,离开作用域会自动销毁。但 m1 和 m2 的生命周期形成了一种「还没脱离作用域,就被提前销毁了」的状况:

fn main() {
    let mut v = 99;

    let m1 = &mut v;
    *m1 += 1;
    // --- m1 在后续不会用到了,提前销毁吧,再见👋 ---
    // 👇 后面的代码可以重新创建引用了 👇
    
    let m2 = &mut v;
    *m2 += 1;
    // --- m2 在后续不会用到了,提前销毁吧,再见👋 ---
    // 👇 后面的代码可以重新创建引用了 👇
    
    // 再来一个只读借用
    let b1 = &v;
    println!("v: {:?}", b1); // 成功打印 101
} // 函数作用域结束,b1 销毁

以上代码标注了变量 m1, m2, b1 的销毁时机。Rust 编译器很聪明,它知道「可变借用 m1」修改完值后(*m1 + 1),在后续代码中再也用不到了。如果这时候 m1 继续占着借用的名额,直到函数结束,在它之后的代码就不能再创建 v 的任何借用了。简直占着茅坑...所以出于效率的考量,如果编译器确定一个借用的生命周期可以提前结束(后面用不到了),就提前把它销毁释放,不用等到整个作用域结束。如此一来,后续的代码就可以重新创建引用/借用了。这就叫做 Non-Lexical Lifetimes(NLL),在以上代码中,m1 和 m2 都符合这种情况。

综上。本节阐述了借用规则的核心。重点只有三句话:

  1. 出借者(所有者)的生命周期,必须大于其引用(借用者)
  2. 同一作用域,可以同时存在多个「只读借用」,但只能存在一个「可变借用」
  3. 「只读借用」和「可变借用」互斥 -- 可读不可写,可写不可读

至此,你掌握了 Rust 的借用规则了,准备好战斗了么?没准备好也没关系,我们要时刻记住:Rust 编译器是我们的良师益友。在你大意时,它会用错误警醒你;在你疑惑时,它会用标注指导你。初学者们只要心中有个大概,就可以勇敢开始写代码了,Rust 编译器会「教你做人」的。

结语

从内存到所有权,再从所有权到借用。Rust 从最底层的问题出发,设计了一套独一无二的编码范式,并通过极其严格的编译检查来「强制」你遵循规范,最终目的是产出「内存安全」的代码。只要你通过了编译器的检查,便不再有内存泄漏、不再有内存污染、不再有 Double Free(内存重复释放)、不再有迷途指针(Dangling Pointer)... 最重要的是,在一次次和编译器斗智斗勇的过程中,你渐渐成为了一个更好的开发者。

别急着踏上冒险,初入 Rust 世界,你还需要了解一个重要知识 -- 生命周期。下一篇见。