Rust 学习笔记 02:变量与可变性

“Stability is not immobility.” – Prince Metternich

习惯了 Go 语言的 var:=,第一次写 Rust 时,最让人抓狂的报错一定是 cannot assign twice to immutable variable

在 Go 里,变量生来就是可变的(除了 const),但在 Rust 里,变量生来就是不可变的。

这就像是:Go 是一个自由奔放的游乐场,你想去哪就去哪;而 Rust 是一个戒备森严的博物馆,除非你申请了"触摸许可"(mut),否则只能看,不能摸。

1. 默认不可变 (Immutability)

在 Rust 中,当你写下:

let x = 5;

注意: Rust 必须在语句末尾加上 “;",与 Java 一样,但是 Go 却不需要,这需要 Go 开发者习惯很久。

这不仅是定义了一个变量,更是立下了一个誓言:“x 的值就是 5,永远不会变。”

如果你试图打破誓言:

x = 6; // 编译报错!

编译器会无情地告诉你:cannot assign twice to immutable variable x

这种设计是为了安全。在并发编程中,如果一个数据大家都只读不写,那就根本不需要锁。Rust 从根源上消灭了很多并发 Bug。

那我想改怎么办?

很简单,加上 mut 关键字,申请"触摸许可”:

let mut x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is now: {}", x);

对于 Go 开发者来说,这需要适应一下。在 Go 里我们很少刻意区分可变/不可变,但在 Rust 里,这是你做每一个决定时的第一考量。

2. 常量 (Constants)

Rust 也有常量,用 const 定义。和 Go 的 const 类似,但有几点不同:

  1. 必须显式标注类型(Go 可以推导)。
  2. 可以在任何作用域定义,包括全局。
  3. 全大写 + 下划线命名(习惯约定)。
const MAX_POINTS: u32 = 100_000;

3. 变量遮蔽 (Shadowing)

这是 Rust 一个非常有趣(风骚)的特性,Go 里没有(或者说不推荐这么做,Go 里同名变量容易导致 bug)。

在 Rust 里,你可以重复定义同名变量:

let x = 5;
let x = x + 1; // 新的 x 遮蔽了旧的 x
let x = x * 2;

println!("The value of x is: {}", x); // 12

这看起来很乱?实际上非常有用。

比如在 Go 里,我们转换类型时常这样写:

str := "42"
num, _ := strconv.Atoi(str) // 不得不换个名字,或者用 num 覆盖

在 Rust 里:

let spaces = "   ";
let spaces = spaces.len(); // 类型都变了!从 &str 变成了 usize

我们不需要发明 spaces_strspaces_num 这种名字,直接 Shadowing 掉之前的变量,反正之前的也不用了。这在处理临时变量转换时非常爽。

4. 基础数据类型概览

Rust 是静态强类型语言。类型推导比 Go 强,大多数时候不用写类型,但也可以显式标注。

标量类型

  • 整型: i8, u8, i16, u16, i32 (默认), u32, i64, u64, i128, u128, isize, usize (类似 Go 的 int)。
    • 吐槽:Go 的 int 到底是 32 还是 64 取决于系统,Rust 分得清清楚楚,而且原生支持 128 位基础类型整数,而 Go 需要使用 big 包,我猜这也是为什么 solana 使用 rust 作为开发语言的原因之一吧。
  • 浮点型: f32, f64 (默认)。
  • 布尔型: bool (true, false)。
  • 字符型: char。注意!Rust 的 char 是 4 字节的 Unicode 标量值,可以存 Emoji 😻,不仅仅是 ASCII。这和 Go 的 rune 类似,但 Go 的 string 底层是 byte 数组,Rust 的 String 处理稍微复杂点(后面讲)。

复合类型

  • 元组 (Tuple): let tup: (i32, f64, u8) = (500, 6.4, 1);。Go 没有元组,只能通过多返回值模拟。
  • 数组 (Array): let a = [1, 2, 3, 4, 5];。长度固定,分配在栈上。这和 Go 的数组一样(不是 Slice)。

5. 小结

第二天,我们重新认识了"变量"。

  • Rust 的 let 默认是不可变的,想变要加 mut
  • Shadowing 是个好东西,可以用来转换类型,复用变量名。
  • 类型系统非常严谨,i32 就是 i32,不能自动转成 i64

下一篇,我们将探讨函数。Rust 的函数有一个让 Go 开发者很不习惯的特性:表达式返回值(不用写 return)。


练习题

  1. 定义一个不可变变量,尝试修改它,看看编译器的报错信息。
  2. 使用 Shadowing,将一个字符串类型的数字(如 “123”)转换为数字类型,并计算它的平方。

思考题

为什么 Rust 允许 Shadowing 而 Go 不允许(或不推荐)?在什么场景下 Shadowing 可能会导致混淆?


本文代码示例


相关阅读