Rust编程:编程中的基本概念

 

第1课介绍的是编程中通用的基础概念,包括 内存分配、堆和栈的区别 数值与类型、指针与引用、函数方法与闭包 并发与并行、同步与异步、泛型编程

第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 个字节。

    mem-1

问题: 数据什么时候放栈上,什么时候放堆上? 栈上存放的数据是静态的,固定大小,固定生命周期;堆上存放的数据是动态的,不固定大小,不固定生命周期

1.2 栈

  • 栈是自顶向下生长
  • 每个函数调用时,会给这个函数分配一块连续内存(函数栈帧)
  • 栈帧的分配必须是确定大小
    • 要确定的内容
      • 这个函数栈先保存自己会用到的通用寄存器的内容,以便函数返回时能恢复被它覆盖的寄存器
      • 还会为自己运行要用到的变量分配内存
      • 其次会主自己要调用的函数分配它的参数内存,以便被调函数可以得到参数
      • 最后还要保存自己执行完后,要返回的地址
    • 函数调用过程的栈帧分配

      mem-2

    • 如何确定的?
      • 通过编译器,编译器把每个函数看成一个最小的编译单元,它分析函数,得知函数会用到什么寄存器,会用到什么变量,这样就可以计算出函数栈帧的大小

为什么不把所有的内容都放到栈上呢? 栈是一个特殊的连续存储结构,所以是有限制的,因为还需要为其他存储结构留下空间。当大量数据都放到栈上,会引起栈溢出。

1.3 堆

堆的分配会使用libcmalloc,堆上的内存都要显式的释放,所以堆上的内存是需要管理的生命周期。当我们没有管理堆上内存的生命周期时,就会造成内存泄漏、野指针。

1.4 理解数据在内存的什么位置

  • 打印在.rodata段中变量的地址
  • 打印在.data段变量的地址
  • 打印在.text段函数的地址
  • 打印在栈上的变量地址
  • 打印在堆上变量的地址
    • String类型
    • Box类型

2 数据

2.1 值和类型

  • 类型:确定值的内存长度、对齐、操作
  • 原生类型:基础数据类型
    • 字符、整数、浮点数、布尔值、数组(array)、元组(tuple)、指针、引用、函数、闭包
    • 固定大小,可分配在栈上
  • 组合类型
    • 结构体(逻辑与)
    • 联合(逻辑否)
      • 枚举

2.2 指针和引用

  • 指针:保存内存地址的类型
    • 通过解引用访问内存
    • 可以解引用到任意类型
  • 引用:保存内存地址的类型
    • 只能解引用到它引用的类型
  • 胖指针
    • 保存内存地址
    • 其他信息

2.3 函数、方法与闭包

  • 函数:完成功能的相关语句
  • 方法:类中定义的函数
  • 闭包:保存代码及环境的数据结构
    • 闭包捕获上下文中的自由变量
    • 成主闭包的一部分

      closure

2.4 接口和虚表

  • 当我们在运行期使用接口来引用具体类型的时候,代码就具备了运行时多态的能力
  • 但是,在运行时,一旦使用了关于接口的引用,变量原本的类型被抹去,我们无法单纯从一个指针分析出这个引用具备什么样的能力
  • 因此,在生成这个引用的时候,我们需要构建胖指针,除了指向数据本身外,还需要指向一张涵盖了这个接口所支持方法的列表。这个列表,就是我们熟知的虚表(virtual table)
  • 下图展示了一个 Vec 数据在运行期被抹去类型,生成一个指向 Write 接口引用的过程

    trait_obj

比如我想为一个编辑器的 Formatter 接口实现不同语言的格式化工具。我们可以在编辑器加载时,把所有支持的语言和其格式化工具放入一个哈希表中,哈希表的 key 为语言类型,value 为每种格式化工具 Formatter 接口的引用。这样,当用户在编辑器打开某个文件的时候,我们可以根据文件类型,找到对应 Formatter 的引用,来进行格式化操作。

3 运行方式

3.1 并发(concurrency)与并行(parallel)

cc

  • 并发
    • 多个任务/事件都可以运行
    • 并发是同时与多件事情打交道的能力,比如系统可以在任务 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

4 学习资料