一条反常识的规矩
在 Rust 里声明一个变量:
let x = 5;
然后你顺手想改它:
let x = 5;
x = 6; // 编译错误!
编译器拒绝了你,理由只有一句:变量默认是不可变的(immutable)——大白话:「没说能改,就不许改」。想声明一个能改的变量,必须明确加上 mut(mutable,可变的):
let mut x = 5; // 这次明确说了「可变」
x = 6; // 没问题
这跟几乎所有主流语言反着来。别处的默认值是「随便改」,想锁死才要加关键字(Java 的 final、JS 的 const);Rust 的默认值是「锁死」,想改才要申请。为什么把默认值设成这样?因为 Rust 的设计者看透了第一章说的那个教训:程序出错的方式千奇百怪,但「一个值在你没注意的时候变了」是最高频的一种。把「不变」设为默认,等于让每个变量自证「我真的需要变」,大量的状态混乱在源头就消失了。等第十二章讲到并发——多个线程同时读写一块内存——你会看到这条小规矩怎么变成救命的堤坝。
类比:普通语言里每个文件默认「谁都能编辑」,出错了再查谁动的;Rust 里每个文件默认「只读」,要编辑得先申请权限。看起来麻烦,但文件夹里再不会出现「咦,这行谁改的?」
类型:编译器会猜,也允许你明说
Rust 是静态类型语言:每个变量的类型在编译期就必须确定下来。但写 let x = 5; 时你并没说类型——编译器会推断:5 是个整数,默认按 i32(32 位有符号整数)处理。大多数时候你就这么写,让编译器猜。猜不准或你想明说时,用冒号标注:
let price: f64 = 9.99; // 64 位浮点数(带小数的数)
let count: u32 = 1_000_000; // 32 位无符号整数(不装负数),下划线只是千分位
let done: bool = false; // 布尔值
let ch: char = '蟹'; // 字符,注意是单引号;一个 char 能装下汉字
整数类型按「多少位、能不能装负数」分成一串:i8/i16/i32/i64/i128(带符号)和 u8/u16/u32/u64/u128(无符号)。拿不准就用默认的 i32,这是社区手感。浮点类似,默认 f64。
有个细节值得知道:Rust 的整数运算不许悄悄溢出。u8 最大只能装 255,如果你让 u8 变量加上超过 255,调试版会直接 panic(程序主动中止并报错),发布版则会绕回(wrap)到 0 重新数——两种都是明确的行为,而不是 C 里那种「看运气」。真需要绕回语义,就明说:x.wrapping_add(1)。又一次,Rust 的选择是:危险的事可以做,但必须写在脸上。
遮蔽:换个写法,不用 mut 也能「变」
Rust 还有个独特的小花招叫遮蔽(shadowing):用 let 再声明一次同名变量,旧的那个就被「遮住」了:
let spaces = " "; // spaces 是个字符串
let spaces = spaces.len(); // 同名,但现在是个数字 3
注意这和 mut 的区别:mut 是同一个变量原地改值,类型不许变;遮蔽是新声明一个变量,只是恰好同名,所以连类型都可以换。上面的代码如果改成 let mut spaces = " "; spaces = spaces.len(); 反而编译不过——字符串不能原地变成数字。
什么时候用哪个?同一个东西「原地改」,用 mut;一个值经过一步步加工、每步换个形态,用遮蔽,既保持了不可变的好处,又不用硬想出 spaces_str、spaces_len 这类笨名字。
常量:连类型推断都不伺候
最后认一个亲戚:常量用 const 声明,必须写明类型,值必须是编译期就能算出来的东西:
const MAX_PLAYERS: u32 = 100;
常量和不可变变量的区别:不可变变量的值可以是运行时才确定的(比如读进来的文件长度),只是定下来之后不许改;常量的值在编译那一刻就烙死在程序里了。约定俗成,常量名全大写、单词间下划线。
这一章的三条小规矩——默认不可变、类型编译期确定、常量烙死——看起来都是写法上的洁癖。但它们有同一个指向:让编译器掌握尽可能多的确定性。编译器知道的越确定,它能替你担保的就越多。下一章我们迎接全书最重的一块石头:所有权。