Skip to content

变量与可变性

Rust 中, 变量 (Variable) 默认不可变:

rust
let a = 0;  // 定义 a 并绑定为 0
// a = 1;   // 尝试为不可变变量 a 赋值 1, 报错

如果需要可变性, 则必须手动添加可变 (Mutable) 关键字显式标记:

rust
let a = 0;
let mut b = 1; // 定义可变的 b 并绑定值 1 给 b;
// a = 2;   // <------- 不允许
b = 3;      // <------- 允许

不过注意, 不可变并非意味着不可初始化:

rust
let a;  // 定义 a
a = 1;  // 绑定 a 为 1, 因为这是 a 第一次被赋值
// a = 2;// 再次尝试绑定, 报错

如此分离定义引入变量绑定, 并无问题, 但若需二次赋值, 则必须显式标记其可变.

类型推断

Rust 是静态强类型语言, 这意味着所有变量的类型都固定且不可变. 但上述代码中, 我们并没有手动写出变量的类型, 那么 Rust 该如何确定变量类型呢?

实际上, Rust 编译器装配了强大的类型推断能力, 只要编译器能够推断出变量的类型, 代码就允许编译通过. 上文代码等价于下述:

Rust
//----------------
let a: i32 = 0;     // 根据初始化值直接推导
//----------------
let a: i32 = 0;
let mut b: i32 = 0;
b = 3;
//----------------
let a: i32;
a = 1;              // 根据后文的初始化推导到前文

但有些时候, 编译器无法成功推导类型, 此时必须手动标注类型. 一般只会出现在部分实现了复杂泛型的堆数据中. 遇到时就能明白为何不能自动推导.

默认类型

一些供于变量初始化的值有众多不同可行的类型, 此时如果不手动指定, 类型推导会使用其中的默认类型, 我们将在本文下面说明默认类型有哪些.

常量

可以用关键字 const 标记常量 (Constant):

Rust
const PI: f64 = 3.1415926;

常量永远不具备可变性, 定义时也必须手动指定其数据类型. 此外, 常量允许定义在任何作用域中, 包括函数体外部:

rust
const PI: f64 = 3.1415926;
fn main() {
    println!("{}", PI);
    const SHIMOKITAZAWA: i32 = 114514;
    println!("{}", SHIMOKITAZAWA);
}

此外, 常量仅允许常量表达式赋值, 也必须定义时就赋值:

rust
const CONST_A: i32 = 114;           // 直接赋值, 无错
const CONST_B: i32 = 114 + 514;     // 常量表达式赋值, 无错, 将在编译期直接得到结果
//-----------------------------------------
// let tmp = 1919;                  // 先定义一个变量,
// const CONST_C: i32 = tmp + 810;  // 然后定义常量为一个变量参与的表达式, 不允许
//------------------------------------------
// const CONST_D: i32;              // 定义常量,
// CONST_D = 12345;                 // 然后赋值, 不可行, 报错.

事实上, Rust 代码检查器设计上就不支持常量类型推导, 因为常量总会内联到使用处直接参与逻辑.

常量之于不可变变量

有人问不可变变量与常量的区别在何处. 不可变变量允许一次初始化赋值, 本次赋值允许 Rust 所有可行的表达式, 而常量仅仅作为某些固定值的助记符而存在, 不具有任何高级功能.

另一个角度, 为什么变量默认不可变? 答案是 Rust 由工程实践得出结论, 在现行语言中, 变量大多数情况下仅需要一次赋值, 而后无需可变, 但绝大多数变量仍可变, 这带来了不可预期的负面影响.

变量遮蔽

Rust 中多次定义相同名称的变量完全可行:

Rust
fn main() {             //<----进入主函数域
    let a = 1;              // 定义 a1: i32 为 1
    let a = 2;              // 遮蔽 a1, 定义 a2: i32 为 2
    let a = "3";            // 遮蔽 a2, 定义 a3: &str 为字符串字面量 "3"
    let a = String::from(a);// 遮蔽 a3, 定义 a4 为 a3 被 String 库的 from 方法转换得到的字符串 "3"
    println!("{a}");
    {                   //<----进入域
        println!("{a}");
        let a = 5.555;      // 遮蔽 a4, 定义 a5: f64 为 5.555
        println!("{a}");
        let a = "6";        // 遮蔽 a5, 定义 a6: &str 为字面量 "6"
    }                   //<----退出域, a6 被释放, 导致 a5 重新可见
                        //     a5 同样因退出域被释放, 导致 a4 重新可见
    println!("{a}");
}                       //<----退出域, a4 被释放.
                        //     a3, a2, a1 依次取消遮蔽, 但同时因为离开作用域, 被释放

编译运行上述代码, 理应得到结果:

shell
"3"
"3"
5.555
"3"

根据我们将会学到的生命周期系统, 可以这么理解:

rust
fn main() {            //-----------------------------------//
    let a = 1;                  // a1                       //
    let a = 2;                  // |  a2                    //
    let a = "3";                // |  |  a3                 //
    let a = String::from(a);    // |  |  |  a4              //
    println!("{a}");            // |  |  |  |<--------------//----- println!("{a}");
    {                       //-----|--|--|--|---------//    //
        println!("{a}");    //  // |  |  |  |<--------//----//----- println!("{a}");
        let a = 5.555;      //  // |  |  |  |  a5     //    //
        println!("{a}");    //  // |  |  |  |  |<-----//----//----- println!("{a}");
        let a = "6";        //  // |  |  |  |  |   a6 //    //
    }                       //-----|--|--|--|--x-- x--//    //
    println!("{a}");            // |  |  |  |<--------------//----- println!("{a}");
}                      //----------x--x--x--x---------------//

后定义的变量将覆盖在先定义的变量上, 当方法或函数希望调用变量时, 仅会访问到最上层的那个.

数据类型

基本的数据类型分两类, 标量类型与复合类型. 前者直接表示量, 后者表示量的组合.

标量类型

整形 (Integer)

顾名思义, 表示整数.

整形按占据比特数多少与是否有符号有如下类型:

长度有符号无符号
8i8u8
16i16u16
32i32u32
64i64u64
128i128u128
archisizeusize

其中 isize 和 usize 占据比特数与操作系统位数 (32/64) 一致.

i8 类型为例, 其在内存中以如下形式存储:

0符号位12345671 Byte/字节 = 8 bit \underbrace{\overbrace{\colorbox{gray}0}^\text{\kern-5pt符号位\kern-5pt}\kern-3pt\fbox{1}\fbox{2}\fbox{3}\fbox{4}\fbox{5}\fbox{6}\fbox{7}}_\text{1 Byte/字节 = 8 bit}

可见 i8 类型能表示的最小数即为 1111 1111Binary=64=1×27 \underline{1111\ 1111}_\text{Binary} = -64 = -1\times 2^7 , 最大数即为 0111 1111Binary=63=271 \underline{0111\ 1111}_\text{Binary}=63=2^7-1 .

补码

关于负数的表示牵涉到补码问题, 不甚重要, 可以自行了解.

nn 字节类型能够表示的数, 总计必然为 2n2^n 个, 对 un\mathrm{u}n 而言, 范围为 02n1 0\sim 2^n-1 , 而对 in\mathrm{i}n 而言, 范围为 2n12n11-2^{n-1}\sim2^{n-1}-1.

除了表示范围及有无符号外, 整形的字面形式也有多种, 不止于我们常见的十进制:

字面值例子写法
Decimal114_514直接写
Hexadecimal0x1234abcd0x 开头
Octal0o1234560o 开头
Binary0b1101_10110b 开头
Byte (u8)b'A'b' ' 包裹的单 ASCII 字符

下划线

数字中的任意下划线均会被忽略, 仅用于视觉效果. 1_2_3_41234 完全同义.

ASCII

一种通用规范, 定义了从 U8:=[0,28)Z\mathbb{U}_8 := \left[0,2^8\right)\cap\mathbb Z 到一些符号的映射表, 有时很好用. 见文末附表.

浮点型 (Float)

浮点是实型变量的内部实现方式, 其他实现方式还有定点型, 几无使用, 不必赘述.

机制

浮点数的机制是总长度固定作为尾数, 小数点根据指数位所描述的浮动, 外带一个符号位, 以 f32 为例大致如下, 采用 IEEE 754 标准, 32 位内存:

0符号128指数, 浮动小数点91031尾数, 指定值 \underbrace{\fbox{0}}_\text{符号}\underbrace{\fbox{1}\fbox{2}\dots\fbox{8}}_\text{指数, 浮动小数点}\underbrace{\fbox{9}\fbox{10}\dots\fbox{31}}_\text{尾数, 指定值}

举例, f32 数字 0 10000101 10101010000000000000000 \underline{ {\color{red}0}\ {\color{violet}10000101}\ {\color{green}10101010000000000000000}} 即表示

(1)0×(2)10000101Binary127×(1+0.10101010000000000000000Binary) (-1)^{\color{red}0} \times (2)^{\underline{\color{violet}10000101}_\text{Binary}-127}\times \left(1+\underline{0.\color{green}10101010000000000000000}_\text{Binary}\right)

=+26×1.6640625=+106.5 = {\color{red}+} 2^{\color{violet}6}\times1{\color{green}.6640625} = +106.5 .

Rust 官方提供了下面几种浮点型, 均具符号位:

长度类型
32f32
64f64

目前关于 16 位及 128 位浮点数, 官方社区仍在讨论.

Bool 类型

rust
let some_boolean: bool = true;

该类型变量有且仅允许有两个可能的值: truefalse. 分别对应逻辑真逻辑假.

概念上看, Bool 类型仅需 11 个比特位即可存储, 但实际上该类型数据需要占据整整 8 bit=1 Byte8\ \text{bit} = 1\ \text{Byte}. 这出于内存寻址机制、健壮性、C/C++ 接口兼容性考虑.

位图

就算采用后文的向量/数组容器 Vec<T>, Vec<bool> 的每一个分量作为 bool 类型仍然占据 11 字节. 此时考虑第三方库 bitvec 所提供的容器, 在底层会将布尔值打包为比特位, 在一些权衡情境下更具优势.

后文介绍的控制语句 if ... {..}, 就必须以一个 Bool 类型变量为控制判据.

字符类型

rust
let some_char: char = 'A';

字符类型允许表示一个 Unicode 标量值, 占据 4 字节. 为什么是 4 字节呢? Rust 要求所有这些标量类型均为定长, 权衡存储效率与性能, 采用了 UTF-32 编码表示字符类型, 恰为 4 字节.

进一步

标准的 UTF-32 编码允许无效代理项 ( U+D800 ~ U+DFFF ) 码点, 但 Rust 中仅允许 Unicode 标量值范围的 UTF-32 ( U+0000 ~ U+D7FFF, U+E000 ~ U+10FFFF ).

注意到两个十六进制数对应一个字节.

复合类型

该类类型允许标量类型间组合.

Tuple 元组

rust
let a = ('A', 1, 3.14);
let b: (i32, f64, u8) = (100, 2.3, 2);
let c = b.0;

如上, 用圆括号和逗号将多个数据括起来, 如 a, 当希望指定类型时书写如 b. 显而易见, 元组允许任意不同类型组合. 如需单独获取某一分量, 用句点和索引指定, 如 c.

组成元组的每个元素必须定长. 这句话并不仅仅是限制, 我们可以定义这种元组:

rust
let a = ('1', (2, 3.0, 4), false, (5.5));

需要注意到单元素元组与元素本身等价.

Array 组

rust
let arr1 = [1,2,3];
let arr2 = [i32; 3]// = [1,2,3];
let arr3 = [3;5];
let a = arr1[0];

显然与元组的区别: 用中括号标记, 定长, 类型唯一.

由于类型唯一, 允许语法糖定义足够长的组, 如 arr2arr3.

此外, 如果需要获取组的某一分量, 按 a 的方式用中括号标明偏移值.

偏移值和索引

可以直接以 C 语言为例:

数组的偏移值, 指的是数组头指针的偏移值:

c
int a[]= {1,2,3};

如此定义一个数组, 实际上是定义一个 int 指针, 我们可以用指针访问数组元素:

c
*a; // == *(a+0) == a[0] == 1
*(a+1); // == *(1+a) == a[1] == 1[a] == 2

换言之, C 语言中的数组只是指针的语法糖. 在此定义下, 自然有必要让数组索引与指针偏移值对齐, 换言之, 以 0 开始而非 1.

绝大多数编程语言都继承了这个习惯, 其中也包括 Rust. 不过一些专精科研计算的语言如 MATLAB 和 R 语言就选择以 1 为索引起始.

模式匹配:

模式与模式匹配是 Rust 中极强大的工具, 此处简要介绍其与元组、组相关的简单语法.

rust
let arr = ("mp4","m4v","mov");
let (mp4, m4v, mov) = arr;
//---------变量遮蔽------------
let arr = ["mp4","m4v","mov"];
let [mp4, m4v, mov] = arr;
// 也可以只取用所需
let [mp4,..] = arr;
let [_,m4v,_] = arr;

如上, 可以通过构建相同形状的 "左值" 去绑定复合类型的右值, 将复合类型的内容提取出来. 此外可以使用通配符 _ 表示匹配元素但不绑定到任何变量, 使用 .. 占位符表示忽略剩余部分.

附表: ASCII 编码表
二进制十进制十六进制缩写插入记号表示法释义
0000 0000000NUL^@空字符 Null
0000 0001101SOH^A标题开始
0000 0010202STX^B正文开始
0000 0011303ETX^C正文结束
0000 0100404EOT^D传输结束
0000 0101505ENQ^E询问字符
0000 0110606ACK^F确认回应
0000 0111707BEL^G响铃字符
0000 1000808BS^H退格
0000 1001909HT^I水平制表符
0000 1010100ALF^J换行
0000 1011110BVT^K垂直制表符
0000 1100120CFF^L换页
0000 1101130DCR^M回车
0000 1110140ESO^N移出/取消变换
0000 1111150FSI^O移入/激活变换
0001 00001610DLE^P数据链路转义
0001 00011711DC1^Q设备控制1
0001 00101812DC2^R设备控制2
0001 00111913DC3^S设备控制3
0001 01002014DC4^T设备控制4
0001 01012115NAK^U确认失败回应
0001 01102216SYN^V同步空闲
0001 01112317ETB^W传输块结束
0001 10002418CAN^X取消
0001 10012519EM^Y介质结束
0001 1010261ASUB^Z替换
0001 1011271BESC^[Escape
0001 1100281CFS^\文件分隔符
0001 1101291DGS^]组分隔符
0001 1110301ERS^^记录分隔符
0001 1111311FUS^_单元分隔符
0010 00003220--空格
0010 00013321--!
0010 00103422--"
0010 00113523--#
0010 01003624--$
0010 01013725--%
0010 01103826--&
0010 01113927--'
0010 10004028--(
0010 10014129--)
0010 1010422A--*
0010 1011432B--+
0010 1100442C--,
0010 1101452D---
0010 1110462E--.
0010 1111472F--/
0011 00004830--0
0011 00014931--1
0011 00105032--2
0011 00115133--3
0011 01005234--4
0011 01015335--5
0011 01105436--6
0011 01115537--7
0011 10005638--8
0011 10015739--9
0011 1010583A--:
0011 1011593B--;
0011 1100603C--<
0011 1101613D--=
0011 1110623E-->
0011 1111633F--?
0100 00006440--@
0100 00016541--A
0100 00106642--B
0100 00116743--C
0100 01006844--D
0100 01016945--E
0100 01107046--F
0100 01117147--G
0100 10007248--H
0100 10017349--I
0100 1010744A--J
0100 1011754B--K
0100 1100764C--L
0100 1101774D--M
0100 1110784E--N
0100 1111794F--O
0101 00008050--P
0101 00018151--Q
0101 00108252--R
0101 00118353--S
0101 01008454--T
0101 01018555--U
0101 01108656--V
0101 01118757--W
0101 10008858--X
0101 10018959--Y
0101 1010905A--Z
0101 1011915B--[
0101 1100925C--\
0101 1101935D--]
0101 1110945E--^
0101 1111955F--_
0110 00009660--`
0110 00019761--a
0110 00109862--b
0110 00119963--c
0110 010010064--d
0110 010110165--e
0110 011010266--f
0110 011110367--g
0110 100010468--h
0110 100110569--i
0110 10101066A--j
0110 10111076B--k
0110 11001086C--l
0110 11011096D--m
0110 11101106E--n
0110 11111116F--o
0111 000011270--p
0111 000111371--q
0111 001011472--r
0111 001111573--s
0111 010011674--t
0111 010111775--u
0111 011011876--v
0111 011111977--w
0111 100012078--x
0111 100112179--y
0111 10101227A--z
0111 10111237B--{
0111 11001247C--|
0111 11011257D--}
0111 11101267E--~
0111 11111277FDEL^?Delete 字符