
从"let 开始"到"类型推断",Rust 的变量比你想象的更有原则
还记得你第一次学编程时写的"Hello World"吗?我猜大概率是这样的:
name = "Larry"
print(f"Hello, {name}!")
简单粗暴,对吧?变量就是个盒子,想装啥装啥,想改就改。
但到了 Rust 这里……事情变得有点"复杂"了。
let name = "Larry";
// name = "Bob"; // ❌ 编译器:我不允许!
我当时就想:这编译器是不是跟我过不去? 为啥不让我改变量?
别急,今天咱们就来聊聊 Rust 的变量和基本类型。你会发现,Rust 的"死板"背后,其实是一套精心设计的哲学。
在其他语言里,变量像个盒子:
┌─────────────┐
│ name │ ← 盒子标签
│ "Larry" │ ← 盒子里的东西
└─────────────┘
你想换东西?把旧的拿出来,塞个新的进去,完事。
但在 Rust 里,变量更像是个标签:
name
↓
┌─────────────┐
│ "Larry" │ ← 数据在内存里
└─────────────┘
let name = "Larry"; 这行代码的意思是:给"Larry"这个数据贴上个叫 name 的标签。
默认情况下,这个标签只能贴一次。你想重新贴?编译器会问你:"你确定吗?"
Rust 有两种常量,就像人有两种性格:
类型 | 特点 | 使用场景 |
|---|---|---|
const | 编译期确定,内联展开 | 配置值、魔法数字 |
static | 运行时存在,有固定地址 | 全局状态、缓存 |
生活化类比:
const 像是印在 T 恤上的图案——编译时就固定了,每件 T 恤都一样static 像是公告栏上的通知——有个固定位置,大家都去看同一个Rust 是强类型语言,而且大部分时候是静态类型。这意味着:
但这不代表你要写一堆类型注解!Rust 有类型推断,就像个聪明的助手:
let age = ; // 编译器:这是 i32
let price = 9.99; // 编译器:这是 f64
let name = "Larry"; // 编译器:这是 &str
你不用告诉编译器类型,它自己能猜出来。但猜错了怎么办?咱们后面说。
fn main() {
// 声明一个不可变变量
let x = ;
println!("x 的值是:{}", x);
// 尝试修改?编译器不允许!
// x = 6; // ❌ 错误:cannot assign twice to immutable variable
// 但可以用 shadowing(影子)"覆盖"
let x = ; // ✅ 这是新的 x,不是修改旧的
println!("x 的新值是:{}", x);
// 甚至可以改变类型!
let x = "现在是字符串了";
println!("x 变成:{}", x);
}
输出:
x 的值是:5
x 的新值是:6
x 变成:现在是字符串了
逐行解释:
let x = 5; - 声明不可变变量 x,绑定到值 5x = 6; - ❌ 被注释掉,因为这会编译失败let x = 6; - ✅ 这是shadowing,新的 x 遮蔽了旧的 xlet x = "..." - shadowing 甚至可以改变类型!(但实际开发中不建议这么玩)fn main() {
let mut count = ;
count = ; // ✅ 可以修改,因为加了 mut
let name = "Larry";
// name = "Bob"; // ❌ 编译错误!
}
编译器错误信息:
error[E0384]: cannot assign twice to immutable variable `name`
--> src/main.rs:6:5
|
5 | let name = "Larry";
| ---- first assignment to `name`
6 | name = "Bob";
| ^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
5 | let mut name = "Larry";
| +++
编译器在说什么人话:
"嘿,你这个
name变量是不可变的(immutable),就像刻在石头上的字,不能改!" "如果你想改,当初声明时就加上mut关键字啊!"
修复方法:
let mut name = "Larry"; // 加上 mut
name = "Bob"; // ✅ 现在可以改了
// 常量必须标注类型,且用大写字母命名
const MAX_POINTS: u32 = 100_000;
const PI: f64 = 3.14159265359;
const GREETING: &str = "你好,世界!";
fn main() {
println!("最大分数:{}", MAX_POINTS);
println!("圆周率:{}", PI);
println!("{}", GREETING);
// 常量可以在编译期计算
const SECONDS_PER_DAY: u32 = * * ;
println!("一天有 {} 秒", SECONDS_PER_DAY);
}
输出:
最大分数:100000
圆周率:3.14159265359
你好,世界!
一天有 86400 秒
关键点:
const 必须标注类型(: u32)// static 变量有固定的内存地址
static LANGUAGE: &str = "Rust";
static mut COUNTER: u32 = ; // ⚠️ 可变 static 需要 unsafe
fn main() {
println!("语言:{}", LANGUAGE);
// 访问可变 static 需要 unsafe 块
unsafe {
COUNTER += ;
println!("计数器:{}", COUNTER);
}
}
⚠️ 警告: 可变 static mut 是不安全的,因为多线程访问会导致数据竞争。实际开发中应该用 Mutex 或 Atomic 类型。
const vs static 对比:
const MAX: u32 = ; // 编译期内联,每次使用都是新副本
static MIN: u32 = ; // 运行时固定地址,全局唯一
fn main() {
let a = MAX; // 相当于 let a = 100;
let b = MAX; // 又是 let b = 100;
let c = &MIN; // 取 MIN 的地址
let d = &MIN; // 同一个地址!
println!("{:p}", c); // 打印地址
println!("{:p}", d); // 和上面一样!
}
fn main() {
// 有符号整数(可正可负)
let a: i8 = -; // 8 位,范围 -128 ~ 127
let b: i16 = -; // 16 位
let c: i32 = -; // 32 位,默认类型
let d: i64 = -; // 64 位
let e: i128 = -; // 128 位
// 无符号整数(只能非负)
let f: u8 = ; // 8 位,范围 0 ~ 255
let g: u32 = ; // 32 位
// 机器字长类型(取决于平台)
let h: isize = ; // 32 位或 64 位系统不同
let i: usize = ; // 常用于数组索引
println!("i32 是默认类型:{}", c);
println!("数组索引用 usize: {}", i);
}
新手坑点:整数溢出
fn main() {
let x: u8 = ;
// let y = x + 1; // ❌ 编译期能检测到的溢出会报错
// 运行时的溢出行为取决于编译模式:
// - debug 模式:panic!
// - release 模式:回绕(255 + 1 = 0)
// 安全做法:用 checked_add
match x.checked_add() {
Some(result) => println!("结果是:{}", result),
None => println!("溢出了!"), // ✅ 会输出这个
}
}
fn main() {
let pi: f32 = 3.14159; // 32 位浮点数
let e: f64 = 2.71828; // 64 位浮点数,默认类型
// 浮点数有科学计数法
let avogadro: f64 = 6.022e23; // 阿伏伽德罗常数
println!("圆周率:{}", pi);
println!("自然常数:{}", e);
println!("阿伏伽德罗常数:{}", avogadro);
// ⚠️ 浮点数比较要小心!
let a = 0.1 + 0.2;
let b = 0.3;
println!("0.1 + 0.2 == 0.3 ? {}", a == b); // 输出 false!
// 正确做法:比较差值是否足够小
let epsilon = 1e-10;
println!("差值小于 epsilon ? {}", (a - b).abs() < epsilon); // true
}
输出:
圆周率:3.14159
自然常数:2.71828
阿伏伽德罗常数:602200000000000000000000
0.1 + 0.2 == 0.3 ? false
差值小于 epsilon ? true
吐槽时间: 是的,0.1 + 0.2 != 0.3,这不是 Rust 的 bug,是所有浮点数的"特性"。因为二进制无法精确表示某些十进制小数。
fn main() {
let is_rust_fun: bool = true;
let is_monday: bool = false;
// 布尔运算
println!("Rust 有趣吗?{}", is_rust_fun); // true
println!("今天是周一吗?{}", is_monday); // false
println!("取反:{}", !is_monday); // true
// 逻辑运算
let can_code = true;
let has_coffee = false;
println!("能写代码且有咖啡?{}", can_code && has_coffee); // false
println!("能写代码或有咖啡?{}", can_code || has_coffee); // true
}
fn main() {
// char 是 Unicode 标量值,占 4 字节
let letter: char = 'A';
let chinese: char = '中';
let emoji: char = '🦀'; // Rust 的吉祥物 crab
// 转义字符
let newline: char = '\n';
let tab: char = '\t';
let quote: char = '\'';
println!("字母:{}", letter);
println!("中文:{}", chinese);
println!("emoji: {}", emoji);
println!("Rust crab: {}", '🦀');
// ⚠️ 注意:char 是单个字符,不是字符串
// let word: char = "Hello"; // ❌ 错误!这是 &str
}
输出:
字母:A
中文:中
emoji: 🦀
Rust crab: 🦀
fn main() {
// 元组:固定长度,可以包含不同类型
let point: (i32, i32) = (, );
let person: (&str, i32, bool) = ("Larry", , true);
// 访问元组元素:用索引
let x = point.; // 第一个元素
let y = point.; // 第二个元素
println!("坐标:({}, {})", x, y);
// 解构:更优雅的访问方式
let (name, age, is_active) = person;
println!("{} 今年 {} 岁", name, age);
// 元组可以嵌套
let nested: ((i32, i32), i32) = ((, ), );
let ((a, b), c) = nested;
println!("解构嵌套:{}, {}, {}", a, b, c);
// 空元组叫"unit type",类似 ()
let nothing: () = ();
}
输出:
坐标:(3, 4)
Larry 今年 25 岁
解构嵌套:1, 2, 3
实战场景:函数返回多个值
fn get_user_info() -> (&'static str, i32) {
("Larry", )
}
fn main() {
let (name, age) = get_user_info();
println!("{} 今年 {} 岁", name, age);
}
fn main() {
// 数组:固定长度,所有元素类型相同
let numbers: [i32; ] = [, , , , ];
// 简化写法:重复某个值
let zeros: [i32; ] = [; ]; // [0, 0, 0, 0, 0]
// 访问元素:用索引
let first = numbers[];
let third = numbers[];
println!("第一个数:{}", first);
println!("第三个数:{}", third);
// 数组长度
println!("数组长度:{}", numbers.len());
// ⚠️ 越界访问会 panic!
// let out_of_bounds = numbers[10]; // ❌ 运行时 panic
// 安全访问:用 get 方法
match numbers.get() {
Some(value) => println!("值是:{}", value),
None => println!("索引越界了!"), // ✅ 会输出这个
}
}
输出:
第一个数:1
第三个数:3
数组长度:5
索引越界了!
数组 vs Vec:
特性 | 数组 [T; N] | Vec Vec<T> |
|---|---|---|
长度 | 编译期固定 | 运行时可变 |
位置 | 栈上 | 堆上 |
性能 | 更快 | 稍慢 |
使用场景 | 固定大小数据 | 动态集合 |
fn main() {
// 编译器能推断出类型
let x = ; // i32
let y = 3.14; // f64
let z = "hello"; // &str
let is_true = true; // bool
// 但有些情况需要显式标注
let a = "42".parse::<i32>().unwrap(); // 需要指定解析成什么类型
// 或者用类型注解
let b: i32 = "42".parse().unwrap();
// 函数返回值也能推断
fn add(x: i32, y: i32) -> i32 {
x + y // 返回类型可以推断,但建议显式标注
}
println!("x + y = {}", add(x, ));
}
什么时候需要显式标注类型?
parse())const 必须标注)错误代码:
let count = ;
count = ; // ❌ 编译错误
错误信息:
error[E0384]: cannot assign twice to immutable variable `count`
解决方案:
let mut count = ; // 加上 mut
count = ; // ✅
我的心理活动: "我就想改个变量,咋这么多规矩?" 编译器的回答: "规矩是为了保护你!"
错误代码:
fn main() {
let a: i32 = ;
let b: i64 = ;
// let c = a + b; // ❌ 类型不匹配
}
错误信息:
error[E0271]: type mismatch resolving `i32 as Add<i64>`
人话翻译: "i32 和 i64 不能直接相加,你得先转换!"
解决方案:
let c = a as i64 + b; // 显式转换
// 或者
let c = a + b as i32; // 但可能溢出!
错误代码:
fn main() {
let arr = [, , ];
// 编译期能检测到的越界会报错
// let x = arr[5]; // ❌
// 但运行时的越界会 panic
let index = ;
let x = arr[index]; // ⚠️ 运行时 panic
}
运行时错误:
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 5'
最佳实践:
// 用 get 方法安全访问
match arr.get() {
Some(value) => println!("{}", value),
None => println!("越界了!"),
}
错误代码:
fn main() {
let a = 0.1 + 0.2;
let b = 0.3;
// if a == b { // ⚠️ 可能永远不相等!
// println!("相等");
// }
}
正确做法:
let epsilon = 1e-10;
if (a - b).abs() < epsilon {
println!("在误差范围内相等");
}
错误代码:
// const MAX = 100; // ❌ const 必须标注类型
// MAX = 200; // ❌ const 不能修改
fn main() {
let x = ;
// const Y = 200; // ❌ const 不能在函数内定义(实际可以,但少见)
}
正确做法:
const MAX: i32 = ; // ✅ 标注类型
fn main() {
let mut x = ; // ✅ 可变变量
x = ;
}
fn main() {
// 摄氏温度转华氏温度
let celsius: f64 = 25.0;
let fahrenheit = celsius_to_fahrenheit(celsius);
println!("{}°C = {}°F", celsius, fahrenheit);
// 华氏温度转摄氏温度
let fahrenheit: f64 = 77.0;
let celsius = fahrenheit_to_celsius(fahrenheit);
println!("{}°F = {}°C", fahrenheit, celsius);
}
fn celsius_to_fahrenheit(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}
fn fahrenheit_to_celsius(f: f64) -> f64 {
(f - 32.0) * 5.0 / 9.0
}
输出:
25°C = 77°F
77°F = 25°C
知识点: 浮点数运算、函数定义、类型推断
fn main() {
// 用元组存储学生信息
let student1: (&str, i32, f64) = ("张三", , 95.5);
let student2: (&str, i32, f64) = ("李四", , 87.3);
// 解构获取信息
let (name, age, score) = student1;
println!("{} 今年 {} 岁,成绩:{:.1}", name, age, score);
// 用数组存储多个成绩
let scores: [f64; ] = [95.5, 87.3, 92.1, 88.9, 91.0];
// 计算平均分
let sum: f64 = scores.iter().sum();
let average = sum / scores.len() as f64;
println!("平均分:{:.2}", average);
// 找最高分
let max_score = scores.iter().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
println!("最高分:{}", max_score);
}
输出:
张三 今年 18 岁,成绩:95.5
平均分:90.96
最高分:95.5
知识点: 元组、数组、迭代器、类型转换
// 用 const 定义配置常量
const MAX_CONNECTIONS: u32 = ;
const TIMEOUT_SECONDS: u64 = ;
const APP_NAME: &str = "我的应用";
const VERSION: &str = "1.0.0";
fn main() {
println!("应用:{} v{}", APP_NAME, VERSION);
println!("最大连接数:{}", MAX_CONNECTIONS);
println!("超时时间:{} 秒", TIMEOUT_SECONDS);
// 编译期计算
const TIMEOUT_MS: u64 = TIMEOUT_SECONDS * ;
println!("超时时间:{} 毫秒", TIMEOUT_MS);
}
输出:
应用:我的应用 v1.0.0
最大连接数:100
超时时间:30 秒
超时时间:30000 毫秒
最佳实践: 配置值用 const,方便统一管理和修改

核心要点回顾:
mut,这是 Rust 的安全哲学const,全局状态用 staticconst 必须标注,parse() 需要指定get() 方法安全访问==,用误差范围判断金句时间:
"Rust 的变量不是盒子,是标签——标签贴上去,默认就不能撕。" "编译器不是跟你过不去,是在保护你免受自己的伤害。" "类型推断是 Rust 给你的温柔,类型安全是 Rust 给你的原则。"
下篇预告:
下一篇咱们聊聊流程控制——if/else、loop、while、for。你以为这些很简单?Rust 的 if 可是能返回值的!敬请期待~
互动问题:
你在其他语言里遇到过哪些变量相关的坑?欢迎在评论区分享!