第1课介绍的是编程中通用的基础概念,包括
- 内存分配、堆和栈的区别
- 数值与类型、指针与引用、函数方法与闭包
- 并发与并行、同步与异步、泛型编程
1 内存
1.1 字符串
let s = "hello world".to_string();
- “hello wolrd”字符串常量,在编译时放在可执行文件的
.rodata
段,在程序加载时分配一个固定的内存地址 - 执行
"hello world".to_string()
时,在堆上分配内存,通过memcpy把"hello world"
拷贝过去 -
let s
在栈上分配变量s
,因为String
是动态容器,所以s
作为智能指针为,我们使用了三个 word:第一个表示指针、第二个表示字符串的当前长度(11)、第三个表示这片内存的总容量(11)。在 64 位系统下,三个 word 是 24 个字节。
问题: 数据什么时候放栈上,什么时候放堆上? 栈上存放的数据是静态的,固定大小,固定生命周期;堆上存放的数据是动态的,不固定大小,不固定生命周期
1.2 栈
- 栈是自顶向下生长
- 每个函数调用时,会给这个函数分配一块连续内存(函数栈帧)
- 栈帧的分配必须是确定大小
- 要确定的内容
- 这个函数栈先保存自己会用到的通用寄存器的内容,以便函数返回时能恢复被它覆盖的寄存器
- 还会为自己运行要用到的变量分配内存
- 其次会主自己要调用的函数分配它的参数内存,以便被调函数可以得到参数
- 最后还要保存自己执行完后,要返回的地址
-
函数调用过程的栈帧分配
- 如何确定的?
- 通过编译器,编译器把每个函数看成一个最小的编译单元,它分析函数,得知函数会用到什么寄存器,会用到什么变量,这样就可以计算出函数栈帧的大小
- 要确定的内容
为什么不把所有的内容都放到栈上呢? 栈是一个特殊的连续存储结构,所以是有限制的,因为还需要为其他存储结构留下空间。当大量数据都放到栈上,会引起栈溢出。
1.3 堆
堆的分配会使用libc
的malloc
,堆上的内存都要显式的释放,所以堆上的内存是需要管理的生命周期。当我们没有管理堆上内存的生命周期时,就会造成内存泄漏、野指针。
1.4 理解数据在内存的什么位置
- 打印在
.rodata
段中变量的地址 - 打印在
.data
段变量的地址 - 打印在
.text
段函数的地址 - 打印在栈上的变量地址
- 打印在堆上变量的地址
- 对
String
类型 - 对
Box
类型
- 对
2 数据
2.1 值和类型
- 类型:确定值的内存长度、对齐、操作
- 原生类型:基础数据类型
- 字符、整数、浮点数、布尔值、数组(array)、元组(tuple)、指针、引用、函数、闭包
- 固定大小,可分配在栈上
- 组合类型
- 结构体(逻辑与)
- 联合(逻辑否)
- 枚举
2.2 指针和引用
- 指针:保存内存地址的类型
- 通过解引用访问内存
- 可以解引用到任意类型
- 引用:保存内存地址的类型
- 只能解引用到它引用的类型
- 胖指针
- 保存内存地址
- 其他信息
2.3 函数、方法与闭包
- 函数:完成功能的相关语句
- 方法:类中定义的函数
- 闭包:保存代码及环境的数据结构
- 闭包捕获上下文中的自由变量
-
成主闭包的一部分
2.4 接口和虚表
- 当我们在运行期使用接口来引用具体类型的时候,代码就具备了运行时多态的能力
- 但是,在运行时,一旦使用了关于接口的引用,变量原本的类型被抹去,我们无法单纯从一个指针分析出这个引用具备什么样的能力
- 因此,在生成这个引用的时候,我们需要构建胖指针,除了指向数据本身外,还需要指向一张涵盖了这个接口所支持方法的列表。这个列表,就是我们熟知的虚表(virtual table)
-
下图展示了一个 Vec 数据在运行期被抹去类型,生成一个指向 Write 接口引用的过程
比如我想为一个编辑器的 Formatter 接口实现不同语言的格式化工具。我们可以在编辑器加载时,把所有支持的语言和其格式化工具放入一个哈希表中,哈希表的 key 为语言类型,value 为每种格式化工具 Formatter 接口的引用。这样,当用户在编辑器打开某个文件的时候,我们可以根据文件类型,找到对应 Formatter 的引用,来进行格式化操作。
3 运行方式
3.1 并发(concurrency)与并行(parallel)
- 并发
- 多个任务/事件都可以运行
- 并发是同时与多件事情打交道的能力,比如系统可以在任务 1 做到一定程度后,保存该任务的上下文,挂起并切换到任务 2,然后过段时间再切换回任务 1
- 并行
- 同时处理多件事情的手段
- 无需上下文切换,就可以同时执行任务1、2
- 并发是能力、并行是手段
- 当我们的系统拥有了并发的能力后,代码如果跑在多个 CPU core 上,就可以并行运行
3.2 同步和异步
- 同步
- 一个任务开始后,后续操作会阻塞直到这个任务结束
- 同步为了保证代码的因果关系
- 异步
- 异步是指一个任务开始执行后,与它没有因果关系的其它任务可以正常执行,不必等待前一个任务结束
- 异步处理完成后的结果,用Promise来保存
- Promise是一个对象,描述在未来某时刻才能获取的结果的值
- Promise的三个状态
- 初始态:Promise未运行
- 挂起态:Promise运行,但未结束
- 结束态,Promise成功解析出一个值(或者失败)
async
定义一个可以并发执行的任务await
触发这个任务并发执行
3.3 编程范式
3.3.1 泛型编程
- 泛型编程通过参数化让数据结构像函数一样延迟绑定
- 泛型编程包含两个层面,数据结构的泛型和使用泛型结构代码的泛型化
- 数据结构的泛型
- 接收具体参数类型,生成具体数据结构
-
参数化类型、参数多态
struct Conn<S> { io: S, state: State, }
S
的具体类型只有在使用Conn
的上下文中才绑定 - 把参数化数据结构理解成生产类型的函数。调用时,接受具体类型的参数,返回携带这些类型的类型
- 为
S
提供TcpStream
类型,则产生Conn
这个类型,其中io
类型是TcpStream
- 这种延迟绑定,让数据结构有更强的通用性,减少代码重复,提高可维护性
- 代码的泛型化
- 使用泛型结构后代码的泛型化
vscode关于错误提示的插件:error lens