Rust学习笔记:变量、数据类型与函数——从let讲起的基础语法
上一篇装好了环境、跑通了 Hello World,这一篇正式进入 Rust 语法。我们先从最基础的三件事开始:变量怎么声明、数据有哪些类型、函数怎么写。这三样在任何语言里都是地基,但 Rust 有一些「反直觉」的设计,值得单独花一篇文章讲清楚。
本文所有代码均已实测通过,每个知识点的运行输出贴在对应小节末尾。
1. let 不可变 vs mut 可变
这可能是 Rust 给新手的第一记闷棍——let 声明的变量默认不可变。
let x = 5;
// x = 6; // ❌ 编译错误:不能给不可变变量赋值
大部分主流语言里,变量天生就能改。Rust 反过来:你要改,得主动声明 mut。
let mut y = 5;
y = 6; // ✅ mut 允许修改
运行输出:
x 初始值: 5
y 初始值: 5
y 修改后: 6
为什么要这样设计?Rust 追求内存安全,编译期就要确定哪些值会变、哪些不会。默认不可变意味着编译器能做出更激进的优化,也能在编译期就拦住一大类 bug(比如无意中改了不该改的值)。需要可变的时候加个 mut,成本很低,但安全性收益很高。
实用建议:写代码时先用
let,编译器报错说需要可变时再加mut。这是一个好习惯。
2. 标量类型
标量(scalar)就是单一值,Rust 有四种基本标量:
| 类型 | 说明 | 示例 |
|---|---|---|
i32 |
有符号 32 位整数(默认整数类型) | -42 |
u32 |
无符号 32 位整数(不能为负) | 42 |
f64 |
64 位浮点数(默认浮点类型) | 3.14 |
bool |
布尔值 | true / false |
char |
Unicode 字符(4 字节,不只是 ASCII) | '🦀' |
let a: i32 = -42;
let b: u32 = 42;
let c: f64 = 3.14;
let d: bool = true;
let e: char = '🦀'; // Rust 的 char 支持 emoji
类型推断:Rust 编译器很聪明,不写类型也能推断:
let inferred_int = 100; // 推断为 i32
let inferred_float = 2.5; // 推断为 f64
运行输出:
i32: -42, u32: 42, f64: 3.14, bool: true, char: 🦀
推断的整数: 100, 推断的浮点数: 2.5
关于整数类型的选择:日常写业务逻辑默认用
i32就够了,性能好且不容易溢出。只有在明确需要非负值(如数组索引、长度)时才用usize,处理位运算时才考虑u8/u16等。这些后续会用到,现在先记住i32是默认选项。
3. 复合类型:元组与数组
标量是单个值,复合类型是把多个值组合在一起。
3.1 元组(Tuple)
元素类型可以不同,长度固定,存在栈上:
let tup: (i32, f64, char) = (500, 6.4, 'Z');
// 两种取值方式:
// 方式一:解构
let (x, y, z) = tup;
println!("元组解构: {x}, {y}, {z}");
// 方式二:.索引(从 0 开始)
println!("元组 .0: {}, .1: {}, .2: {}", tup.0, tup.1, tup.2);
3.2 数组(Array)
元素类型必须相同,长度也固定,同样存在栈上:
let arr = [1, 2, 3, 4, 5];
println!("第一个元素: {}", arr[0]);
println!("数组长度: {}", arr.len());
// 快捷创建:let arr = [初始值; 数量]
let zeros = [0; 5]; // [0, 0, 0, 0, 0]
数组的 {:?} 是调试格式输出,打印时用:
println!("zeros 数组: {zeros:?}"); // [0, 0, 0, 0, 0]
运行输出:
元组解构: 500, 6.4, Z
元组 .0: 500, .1: 6.4, .2: Z
数组第一个元素: 1
数组长度: 5
zeros 数组: [0, 0, 0, 0, 0]
数组和元组都是固定长度的,存栈上,访问快。需要动态长度的集合后面会学
Vec(向量)。目前只和固定长度打交道。
4. 函数:参数 & 返回值
Rust 函数用 fn 声明,参数必须标注类型,返回值用 -> 指定:
fn add(a: i32, b: i32) -> i32 {
a + b // 最后一个表达式不加分号 → 这就是返回值
}
这里有个 Rust 特有的概念:表达式 vs 语句。
- 语句:执行操作但不返回值,以分号结尾。
let x = 5;是语句。 - 表达式:计算出一个值,不加分号。
a + b是表达式。
所以 add 函数里 a + b 后面没加分号,它的值就是函数的返回值。如果加了分号 a + b;,就变成了语句,函数什么都不返回(实际上是返回 ()——见下文)。
再看一个用 if 当表达式的例子:
fn max(a: i32, b: i32) -> i32 {
if a > b {
a // if 分支,返回 a
} else {
b // else 分支,返回 b
}
}
这里的 if-else 整体是一个表达式,最后的值就是函数的返回值。这和 C/Java 的三元运算符 a > b ? a : b 类似,但 Rust 不需要单独的语法糖——if 本身就支持。
无返回值的函数
fn print_hello() {
println!("这是一个无返回值的函数");
}
没有写 -> 返回类型,函数返回的是 (),叫单元类型(unit type)——可以理解为「空」,类似其他语言里的 void。
let nothing = print_hello();
println!("print_hello 的返回值: {nothing:?}"); // ()
运行输出:
add(10, 20) = 30
max(5, 9) = 9
这是一个无返回值的函数
print_hello 的返回值: ()
5. const 常量
const 声明的是编译期常量,和 let 有本质区别:
const PI: f64 = 3.1415926;
const MAX_POINTS: u32 = 100_000; // 下划线提高可读性,编译时会被忽略
| 对比 | let |
const |
|---|---|---|
| 类型标注 | 可选(有类型推断) | 必须显式标注 |
| 可变性 | 默认不可变,mut 可变 |
永远不可变 |
| 作用域 | 可以在函数体内 | 全局或模块级 |
| 计算时机 | 运行时绑定 | 编译期确定 |
简单记:值在编译时就能确定的、全局共享的、永远不变的,用 const;其余用 let。
运行输出:
常量 PI: 3.1415926
常量 MAX_POINTS: 100000
6. 变量隐藏(Shadowing)
Shadowing 是 Rust 里一个容易让新手困惑但非常实用的特性:用 let 重新声明同名变量,新的会「盖住」旧的。
let x = 5;
let x = x + 1; // 新的 x = 6,旧的 x 被隐藏
Shadowing 有两个 mut 做不到的能力:
能力一:跨作用域生效
let x = 5;
{
let x = x * 2; // 内部作用域:x = 10
println!("内部: {x}"); // 10
}
println!("外部: {x}"); // 5——离开作用域,恢复原值
内部用 let 重新绑定的 x 只在花括号内有效,出来就恢复。这和 mut 完全不同——mut 改了就真改了,出作用域也不还原。
能力二:改变类型
let spaces = " "; // &str 字符串切片
let spaces = spaces.len(); // usize 数字——完全合法
mut 做不到这件事:
// let mut spaces = " ";
// spaces = spaces.len(); // ❌ 类型不匹配:usize 不能赋给 &str
Shadowing 本质是创建了一个新变量,只是碰巧和旧的同名。新变量可以有不同的类型,因为它们是两个完全独立的绑定。
运行输出:
第一次 x: 5
第二次 x (shadowing): 6
内部作用域 shadowing: 12
离开内部作用域后 x: 6
" " 的长度: 3
总结
这一篇覆盖了 Rust 基础语法中最核心的六个概念:
let默认不可变——Rust 的安全哲学从变量声明就开始了- 四种标量类型——记牢
i32是默认整数、f64是默认浮点 - 元组和数组——一个混搭、一个同类型,都固定长度存栈上
- 函数——参数必带类型,最后一个表达式不加分号就是返回值
constvslet——编译期常量 vs 运行时绑定- Shadowing——
mut做不到的两件事:跨作用域恢复、改变类型
下一篇进入控制流:if、loop、while、for,以及 Rust 最优雅的 match 表达式。

浙公网安备 33010602011771号