变量与可变性:Rust 的内存哲学第一课

说实话,我刚开始学 Rust 时,最不习惯的就是它的变量系统。别的语言里,let 就是个声明关键字,但在 Rust 里,它背后藏着一整套内存安全的设计哲学。今天我们就来聊聊这个。

变量声明:let 的第一次亲密接触

在 Rust 里,声明一个变量用 let 关键字。就这么简单:

let x = 5;
println!("x 的值是:{}", x);

嗯,这里要注意。默认情况下,变量是不可变的。什么意思?就是你不能改它:

let x = 5;
x = 6; // 编译错误!

我第一次写 Rust 时就被这个坑过。当时心想:「什么鬼?变量不能变?」后来才明白,这是 Rust 故意设计的。不可变性让代码更容易推理,也避免了多线程下的数据竞争。

我的习惯:能用不可变就用不可变。只有明确需要修改时,才考虑加 mut。这个习惯帮我避免了不少 bug。

可变性:mut 的正确打开方式

如果你确实需要改变一个变量的值,那就加个 mut

let mut y = 10;
y = 20; // 没问题
println!("y 现在是:{}", y);

说白了,mut 就是一把钥匙。你主动告诉编译器:「我知道这个变量会变,我负责保证它的安全。」

我曾经踩过的坑:在循环里滥用 mut,导致代码逻辑混乱。后来我学会了:只在必要的作用域内使用可变性,用完就丢。

常量:const 的硬核规则

常量跟不可变变量有点像,但更严格。用 const 声明:

const MAX_POINTS: u32 = 100_000;

注意几个关键点:

  • 必须显式标注类型(不能省略)
  • 值必须在编译期确定
  • 命名惯例是全大写加下划线
  • 可以在任何作用域声明,包括全局

我个人习惯把配置类的魔法数字都写成 const。比如数据库连接池大小、超时时间这些。这样改起来方便,也不怕手滑写错。

变量遮蔽:Shadowing 的巧妙用法

这个特性很有意思。你可以用同一个名字声明新变量,把旧的「遮住」:

let x = 5;
let x = x + 1; // 遮蔽了之前的 x
let x = x * 2;
println!("x 最终是:{}", x); // 输出 12

你想想看,这跟 mut 有什么区别?

区别大了:

  • 遮蔽可以改变变量的类型
  • 遮蔽创建的是新变量,旧变量依然存在(只是被遮住了)
  • 遮蔽可以改变不可变性

举个例子:

let spaces = "   ";        // 字符串类型
let spaces = spaces.len(); // 数字类型,没问题!

如果用 mut,上面这段代码会报错。因为 mut 不允许改变类型。我在处理用户输入时经常用这个技巧——先拿字符串,再转成数字,全程用一个名字,代码干净多了。

数据类型:标量类型

Rust 是静态类型语言,编译时必须知道所有变量的类型。但编译器很聪明,大部分时候能推断出来。

整数类型

长度 有符号 无符号
8 位 i8 u8
16 位 i16 u16
32 位 i32 u32
64 位 i64 u64
128 位 i128 u128
架构相关 isize usize

默认整数类型是 i32。我个人建议:除非有特殊需求,否则就用 i32。性能好,兼容性强。

避坑指南:整数溢出在 debug 模式下会 panic,release 模式下会回绕。我曾经在嵌入式项目里被这个坑过——release 版本跑得好好的,debug 版本直接崩溃。后来我学会了用 wrapping_add 等方法来显式处理溢出。

浮点类型

两种:f32f64。默认是 f64:

let x = 2.0;    // f64
let y: f32 = 3.0; // f32

浮点数比较要小心。我记得有次做金融计算,直接用 == 比较两个浮点数,结果出了大问题。后来改用 (a - b).abs() < epsilon 这种方式。

布尔类型

就两个值:truefalse。大小是一个字节:

let is_ok = true;
let is_not: bool = false;

字符类型

Rust 的 char 是 Unicode 标量值,占 4 个字节。这意味着它能表示任何语言的字符:

let c = 'z';
let emoji = '😊';
let chinese = '中';

嗯,这里要注意。char 用单引号,字符串用双引号。别搞混了。

复合类型:元组和数组

元组:把不同类型打包

元组可以包含不同类型的值,长度固定:

let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup; // 解构
println!("y 的值是:{}", y);

也可以用索引访问:

let first = tup.0;
let second = tup.1;

我在写函数返回多个值时特别喜欢用元组。比如返回 (状态码, 消息, 数据) 这种组合。

数组:同类型元素的集合

数组长度固定,所有元素类型相同:

let arr = [1, 2, 3, 4, 5];
let first = arr[0];

也可以显式声明类型和长度:

let arr: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 3]; // [0, 0, 0]
我曾经犯过的错:访问数组越界。Rust 会在运行时检查索引是否越界,越界就 panic。所以写循环时一定要小心边界条件。

知识体系总览

下面这张图帮你理清本章的知识结构:

变量与可变性 let 声明 mut 可变性 const 常量 Shadowing 遮蔽 标量类型 复合类型 整数 (i32, u8...) 浮点 (f32, f64) 布尔 (bool) 字符 (char) 元组 (tuple) 数组 (array) 核心思想:不可变优先,显式声明可变 类型安全 + 内存安全 = Rust 的承诺

这张图把本章的知识点串起来了。从中心的「变量与可变性」出发,向外辐射出六个核心概念。标量类型和复合类型又各自展开。你仔细看看,会发现 Rust 的变量系统其实很有条理。

总结一下:
  • let 声明变量,默认不可变
  • mut 开启可变性,需要你主动选择
  • const 声明编译期常量,类型必须标注
  • Shadowing 可以复用变量名,甚至改变类型
  • 标量类型:整数、浮点、布尔、字符
  • 复合类型:元组(不同类型打包)、数组(同类型集合)

这些概念是 Rust 的基石。我当年学的时候,花了一周才真正理解「不可变」的好处。现在回头看,这个设计太聪明了——它把很多运行时才能发现的问题,提前到了编译阶段。

好了,这一章就到这里。记住:变量声明不是小事,它关乎整个程序的安全性。多写、多练、多思考,你很快就能上手。


公众号:蓝海资料掘金营,微信deep3321