:::warning 本章内容改编自《Rust for Rustaceans: Idiomatic Programming for Experienced Developers》第1章 :::

类型与值

与大多数程序设计语言类似,Rust中的一个类型(type)可以被理解为一个集合,其中存放了该类型包含的一组值(value)。给定一个类型T、该类型的一个值v,我们也通常称:值v是类型T的一个实例(instance)。如果在程序的执行过程中创建了值v,我们也称之为:创建了类型T的一个实例。
需要注意的是:正如一个元素可以出现在多个集合中,一个逻辑上的值,自然也可能具有多个类型。例如,如果只给出3这样一个孤零零的值,那么,它的类型既可以被理解为i8(Rust中的一个类型,包含了整数区间[-128, 127)中的所有整数),也可以被理解为u32(Rust中的一个类型,包含了整数区间[0, 232-1)中的所有整数)。
但是,在Rust中,一个值只能具有一个类型。为了消除这种类型的多样性,对于Rust编译器而言,它会为每一个值附带上类型信息(即:这个值所在的那个集合)。即便你在Rust程序中声明了一个值,但没有指定它的类型,Rust编译器也会根据这个值的上下文,为它推断出一个类型(称为 type inference);如果无法推断出一个值的类型,则会报出编译错误。
Rust为每一种类型都默认确定了它的值在内存中的表示方式(memory representation),也即:如何把一个值表示为一个二进制串。如果你对一个类型的默认内存表示不满意,Rust还允许我们为该类型选择一种表示方式,例如:采用与C语言相同的内存表示方式。

内存区域分类

Rust程序在执行时,会涉及到内存中若干不同类型的区域。

Static Memory / 静态内存空间

Rust程序在执行前,首先会被加载到一个名为静态内存空间(static memory)的区域中。这个区域中至少包含两类成分:

  1. 程序的二进制代码
    1. 一般情况下,程序的二进制代码应该是只读的
    2. 在一些特殊情况下,是否会修改二进制代码呢?对于这一点,我不是太确定;走着看吧。理论上,只要通过某种方式获得对二进制区域的读权限,自然可以去修改。但是,修改后是否会带来什么问题,就不好说了。
  2. 程序中使用static关键词声明的变量(即:静态变量)、以及程序中出现的一些常量(如字符串常量)

    1. 静态变量的生存期与程序运行的生存期是相同的:程序开始运行时,这个静态变量就存在了;程序运行结束前,这些静态变量始终都存在。 :::warning Static memory是否还存在其他成分?我还不确定。如果有新的成分,再对这一部分的内容进行更新。 :::

      Stack / 栈空间

      Stack这种内存区域的名称来源于一种被称为“栈”的数据结构。而且,一个栈空间中存放的确实是一个栈类型的数据结构。
      在数据结构中,栈指的一种先进后出的队列。如果无法理解这种解释,那么,可以把一个栈形象地想象为若干上下堆放的箱子:当来了一个新的箱子,我们只能把它放在这一堆箱子的最上边;而如果要从这一堆箱子中取出一个箱子,那么,我们只能取最上层的那个箱子。向栈中加入箱子的操作称为入栈或压入(push);从栈中取出箱子的操作称为出栈或弹出(pop)。
      在Rust程序的运行过程中:
  3. 当发生了一次函数调用f(...)时,一种称为栈帧(stack frame)的元素就会被压入到一个栈空间中。每一个函数f都会具有一个对应的栈帧。这个栈帧规定了如何对函数f中出现的变量进行排布 。例如,如果函数f中出现了一个变量b,则f的栈帧可能会规定:b应该被存放在从栈帧中的第4个字节开始的连续两个字节中。

  4. 在函数调用f(...)的执行过程中,当为一个变量a赋值后,这个值就会被存放在a对应的内存位置中。
  5. 当函数调用f(...)执行结束后,它对应的栈帧就会从所在的栈空间中弹出。其中存在的所有变量自然也都灰飞烟灭了。

    Heap / 堆空间

    堆这种内存区域可以被理解为一个大的内存空间池(a pool of memory)。堆的作用大概有两个:

  6. 存放非定长数据。如果要把一个非定长数据类型的某个值/实例存放在内存中,那么,应该只能把它存放在堆中。

  7. 让数据的生存期变得更长。如果把数据存放在栈帧中,那么,当栈帧从栈中弹出后,数据就不复存在了。如果把数据存放在堆中,则会打破这种生存期限制。

    进程与线程

    进程(Process)可以理解为行进中的程序,也即:程序执行的载体。线程(Thread)可以理解为程序执行中的一条线索。你也可以把进程形象地理解为一部正在上演中的小说,线程则是小说中正在发生的故事线索。
    在一般情况下:
  • 一个进程包括1或多个线程。其中,存在一个线程,称为当前进程的主线程(main thread)
  • 一个进程具有一个堆空间
  • 每一个线程都具有唯一属于自己的一个栈空间

基本概念 - 图1

内存相关的若干术语

Address / 地址

一个程序在运行时使用的内存空间,可以理解为一组连续的二进制位。其中,每一个二进制位中只能存放一个0或1值。
对内存进行有效使用的前提是能够对它进行有效的管理。操作系统对内存进行管理的基本途径就是对内存进行编号,就好像一栋大楼的管理者要对其中的每一个房间进行编号一样。内存编号的基本方式如下:

  • 首先,确定编号的长度,即:采用多少位的二进制数对内存进行编号。每一种计算机硬件都会有一个固定的编号长度。在目前大部分的计算机硬件中,编号长度是32和64中的一种。
  • 然后,从第一个二进制位开始,以8个连续的二进制位(一个字节)为一组对内存空间进行编号。例如,假设编号长度为32,则内存空间中的第一个字节的编号为0x0000;第二个字节的编号为0x0001;以此类推。
  • 这样,内存空间就被划分成了若干带有编号的字节。其中,每一个字节的编号,称为这个内存的地址。

对于一个变量,它所代表的内存位置就是内存空间中被编号的若干个连续字节。其中,这些连续字节的第一个字节的编号,称为变量的地址
内存空间编号的长度,称为地址的宽度。 :::info 你可能会问:为什么称为地址的宽度,而不是地址的长度呢?其实,这不是一个本质性的问题;你习惯于使用哪种名称,都可以。
如果非要为“宽度”找一个原因,可以这样来理解:在计算机硬件中,地址信息是通过地址总线进行传输的。

  • 总线可以类比于高速公路,存在若干条车道,表示可以同时并行行驶的车的数量。车道数越多,则表示告高速公路越宽。
  • 地址总线的宽度,表示其中存在多少条电线。其中,每一条电线在某一时刻可以表示一个0或1值。例如:若电线上无电压,则表示0值;若电线上存在一个正电压,则表示1值。
  • 从提高计算性能的角度出发,在主流计算机中,地址的宽度与地址总线的宽度是相同的。

现在,你知道“地址宽度”这种命名方式的由来了吧。Nice… :::

Place / 位置

对于Rust中的一个值(带有类型信息)而言,它的表示方式,与它的存放位置无关。但是,当程序中出现了一个值,那么,这个值通常会被存放在内存中的某个位置(place)。所谓内存中的某个位置,就是内存中具有连续地址的若干字节。因此,内存中的一个位置,可以用两条信息进行定位:1. 这个位置中首字节的地址;2. 这个位置中包含的字节数。 :::info 基于上述位置定义,内存中的任意一段连续二进制位,可能无法被称为一个位置。 :::

Variable / 变量

Rust中的变量(variable)表示的是栈空间中的一个位置。变量具有三种体现形式:

  1. 函数执行时,函数的一个形式参数的存放位置;这种变量具有名称
  2. 表达式求值过程中一个中间计算结果的存放位置;这种变量不具有名称
  3. 函数执行时,一个局部变量的存放位置;这种变量具有名称

在Rust中,每一个变量都具有一个在编译时刻被确定的类型,表示这个变量对应的内存位置只能存放这种类型的值。
Rust中变量的类型必须是一种定长类型(sized type)。所谓定长类型,指的是该类型的所有值在内存中都会占据相同长度的区域,且这个长度在编译时刻就能确定。因为,只有变量的类型是定长类型时,编译器才有可能确定如何在栈帧中放置这个变量。 :::info 你可能会有一个疑问:难道在Rust中不能声明一个类似C++语言中向量(Vector)类型的变量吗?
在C++中,一个向量类型的变量可以包含多个元素,且可以在运行时向一个向量中添加新的元素。因此,看起来向量是一种非定长类型。进而,看起来Rust无法声明一个向量类型的变量。
有这个疑问,说明你在认真思考。不过,不用担心Rust的表达能力。Rust确实可以声明一个向量类型的变量。产生这个疑问的原因在于我们目前对定长类型这个概念的介绍和理解还存在一些模糊的空间。耐心等一等,很快就会水落石出了。 ::: 如果一个变量具有名称,则程序员可以通过变量名称访问变量代表的位置。但是,一旦源代码被编译为二进制代码后,变量的名称就不复存在了。在编译过程中,编译器会将变量的名称转换为该变量在栈帧中的相对位置。当一个栈帧被压入栈空间中后,栈帧中的每一个变量就会真正对应到内存中的一个位置上。 :::info 在Rust的官方文档中,不存在静态变量(static variable)的概念。与静态变量最相近的一个概念是static item。时机成熟时,我们再对这个概念进行说明。 :::

Pointer /指针

指针是一种特殊的类型,其中的每一个值表示内存空间中的一个地址。通过这个地址,可以定位到内存空间中的一个字节。
一个指针类型的值,可以被存放在内存中的特定位置。例如,可以将一个指针类型的值存放在一个局部变量中。
指针是一种定长类型。因为,我们总是可以在编译时刻确定目标平台上地址的宽度,也即:一个指针类型的值在存储时需要占用多少个二进制位。


:::warning 未完待续 :::