02|所有权(上):Rust是如何管理程序中的资源的?
你好,我是悦创。
今天我们来讲讲 Rust 语言设计的出发点——所有权,它也是 Rust 的精髓所在。
在第一节课中,我们了解了 Rust 语言里的值有两大类:一类是固定内存长度(简称固定尺寸)的值,比如 i32、u32、由固定尺寸的类型组成的结构体等;另一类是不固定内存长度(简称非固定尺寸)的值,比如字符串 String。这两种值的本质特征完全不一样。而怎么处理这两种值的差异,往往是语言设计的差异性所在。
就拿数字类型来说,C、C++、Java 这些语言就明确定义了数字类型会占用内存中的几个字节,比如 8 位,也就是一个字节,16 位,也就是两个字节。而 JavaScript 这种语言,就完全屏蔽了底层的细节,统一用一个 Number 表示数字。Python 则给出了 int 整数、float 浮点、complex 复数三种数字类型。
Rust 语言因为在设计时就定位为一门通用的编程语言(对标 C++),它的应用范围很广,从最底层的嵌入式开发、OS 开发,到最上层的 Web 应用开发,它都要兼顾。所以它的数字类型不可避免地就得暴露出具体的字节数,于是就有了 i8、i16、i32、i64 等类型。
前面我们说到,一种类型如果具有固定尺寸,那么它就能够在编译期做更多的分析。实际上固定尺寸类型也可以用来管理非固定尺寸类型。具体来说,Rust 中的非固定尺寸类型就是靠指针或引用来指向,而指针或引用本身就是一种固定尺寸的类型。
1. 栈与堆
现代计算机会把内存划分为很多个区。比如,二进制代码的存放区、静态数据的存放区、栈、堆等。
栈上的操作比堆高效,因为栈上内存的分配和回收只需移动栈顶指针就行了。这就决定了分配和回收时都必须精确计算这个指针的增减量,因此栈上一般放固定尺寸的值。另一方面,栈的容量也是非常有限的,因此也不适合放尺寸太大的值,比如一个有 1000 万个元素的数组。
那么非固定尺寸的值怎么处理呢?在计算机体系架构里面,专门在内存中拿出一大块区域来存放这类值,这个区域就叫“堆”。
2. 栈空间与堆空间
在一般的程序语言设计中,栈空间都会与函数关联起来。每一个函数的调用,都会对应一个帧,也叫做 frame 栈帧,就像图片栈空间里的方块 main、fn1、fn2 等。一个函数被调用,就会分配一个新的帧,函数调用结束后,这个帧就会被自动释放掉。因此栈帧是一个运行时的事物。函数中的参数、局部变量之类的资源,都会放在这个帧里面,比如图里 fn2 中的局部变量 a,这个帧释放时,这些局部变量就会被一起回收掉。
函数的调用会形成层级关系,因此栈空间中的帧可能会同时存在很多个,并且在它们之间也对应地形成层级关系。如上图所示,可能的函数调用关系为,main 函数中调用了函数 fn1,fn1 中调用了函数 fn2,fn2 中调用了函数 fn3,fn3 中调用了函数 fn4,fn4 调用了更深层次的其他函数。这样的话,在程序执行的某个时刻,main 函数、fn1、fn2、fn3、fn4 等对应的帧副本就同时存在于栈中了。
图中右边堆空间里面的一些小圈表示堆空间中资源,也就是被分配的内存。从图中可以看到,栈空间中函数帧的局部变量是可以引用这些堆上资源的。一个栈帧中的多个局部变量可以指向堆中的多个资源,如 fn3 中的 b 指向资源 A,c 指向资源 B;同时存在的多个栈帧中的局部变量还可以指向堆上的同一个资源,如图中的 a 和 b,c 和 d;堆上的资源也可以存在引用关系,如图中的 D 和 E。
如果一个资源没有被任何一个栈帧中的变量引用或间接引用,如图中的 C,那么它实际是一个被泄漏的资源,也就是发生了内存泄漏。被泄漏的资源会一直伴随程序的运行,直到程序自身的进程被停止时,才会一起被 OS 回收掉。
而计算机程序内存管理的复杂性,主要就在于堆内存的管理比较复杂——既要高效,又要安全。
这里我们稍微提及了一点计算机的结构知识,你可以停下来仔细理解这张图示表达的意思,在后面我们还会经常回顾这张图。有了栈和堆的知识作为铺垫,你会更容易理解 Rust 中的一些特性为什么要那样设计。
下面我们回到 Rust 语言,继续讲 Rust 中另一个重要概念——可变性。
3. 变量与可变性
回顾第一讲的知识,在 Rust 中定义一个变量,使用 let variable = value;
这种语法。比如 let x = 10u32;
,就定义了变量 x。然后,10u32
是一个值,它被绑定到这个变量上。
默认变量是不可变的,我们来做个实验。
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
输出:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| -
| |
| first assignment to `x`
| help: consider making this binding mutable: `mut x`
3 | println!("The value of x is: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
Rust 默认这样做是为了减少一些很低级的 Bug。假如默认可以改的话,如果你在一个代码量很大而且离定义变量很远的某个分支语句里面修改了这个变量的值,然后在后面某个函数调用里面又用到了它,结果导致程序行为与期望不符,这时你很难看出来问题出在哪儿。这种低级错误能不犯就不犯,Rust 干脆帮你禁用了这种方式。
但是下面这样做是可以的。
fn main() {
let x = 5;
println!("The value of x is: {x}");
let x = 6; // 注意这里,重新使用了 let 来定义新变量
println!("The value of x is: {x}");
}
这种方式在 Rust 中叫做变量的 Shadowing。意思很好理解,就是定义了一个新的变量名,只不过这个变量名和老的相同。原来那个变量就被遮盖起来了,访问不到了。这种方式最大的用处是程序员不用再去费力地想另一个名字了!变量的 Shadow 甚至支持新的变量的类型和原来的不一样。
比如:
fn main() {
let a = 10u32;
let a = 'a';
println!("{}", a);
}
那如果我们要修改变量的值应该怎么做呢?只需要在变量名前面加一个 mut 就可以声明一个变量为可以修改内容的。
let mut x = 10u32;
例子:
fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
// 输出
The value of x is: 5
The value of x is: 6
注意,值的改变只能在同一种类型中变化,在变量 x 定义的时候,就已经确定了变量 x 的类型为数字了,你可以试试将其改成字符串,看会报什么错误。
这里你可以回过头去对比一下,可修改变量和变量的 Shadow 的不同之处。
一个变量,其内容是否可变,被称作这个变量的可变性(mutability)。mut 叫作可变性修饰符(modifier)。
可能你会非常疑惑,变量不就应该是会变化的吗? 既然默认不可变,为什么要称其为变量呢?其实上面一段我已经回答了这个问题,Rust 中变量的可变性是一种潜力,只要它有可能会变化,那么就可以称之为变量。而 Rust 给这种潜力加了一道开关,当你想让这个变量的可变性暴露出来的时候,就在变量名前面明确地加个 mut 修饰符。
可以看到,变量名加了 mut,多打了 4 个字符,这实际是在代码中留下了一种足迹。也就是说给了程序员一个信息,当你自己或别的程序员在读到这个变量的定义时,他会知道,后面一定会修改这个变量,因为如果你后面没修改它,Rust 编译器会提示你把这个 mut 去掉。
这种设计还有一个好处,那就是减少滥用概率。我们在这里构造一个编程语言界的墨菲定律,如果一个特性不太利于程序的健壮性,但是很好用,滥用的成本非常低,那么它一定会被滥用。
比如 TypeScript 中的 any 类型,有时写 TS 代码懒得去设计类型,直接就用 any 类型了,反正“先跑通了再说”。结果就是最后项目完成了,代码里面 any 满天飞,TS 的设计初衷被抛至脑后。偷懒是人的天性,Rust 接受了这种天性,让你想要修改一个变量的时候,需要多付出点成本,也就是多打 4 个字符。
另一个例子是 JS 中的 var 和 let,都是三个字符,敲的字符数一样,成本一样,结果就是在语言层面并不能驱动程序员往好的实践方面靠。有人会辩称,在这些语言中会有推荐规范或强制要求,要求你按好的实践方式写。不过在实际项目中,由于进度等问题,这些规范总是很难完全贯彻下去,即使贯彻下去也很难达到预期效果,这方面已有太多案例了。因为那些都是补救措施,哪有从语言层面强制约束你做来得统一。
4. 变量的类型
欢迎关注我公众号:AI悦创,有更多更好玩的等你发现!
公众号:AI悦创【二维码】
AI悦创·编程一对一
AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh
C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh
方法一:QQ
方法二:微信:Jiabcdefh
- 0
- 0
- 0
- 0
- 0
- 0