变量与数据类型:从"默认不可变"说起
Rust核心语法:安全与优雅的代码基石
上一篇我们初步认识了Rust的核心优势,搭建了开发环境,还写了一个复刻Linux wc命令的小工具。但要写出真正符合Rust哲学的**"安全、高性能"**代码,核心语法的掌握是基础------特别是Rust和其他语言差异极大的「默认不可变变量」「match表达式」「Option空值处理」「堆上集合」,这些是Rust"编译时安全检查"的核心载体。
今天我们就来系统学习这些核心语法,最后用一个「学生成绩管理系统」的实战案例,把所有知识点串起来,让你真正理解Rust语法的**"安全设计"和"优雅表达"**。
🔢 2.1 变量与数据类型:从"默认不可变"说起
变量是所有编程语言的基础,但Rust的变量设计从根本上颠覆了我们的传统认知 ------默认不可变。这不是"语法糖",而是Rust"安全优先"设计哲学的直接体现。
2.1.1 变量:默认不可变的设计逻辑
在C/Java/Python中,变量默认是可变的------你可以随时修改它的值。但Rust认为:大多数变量在生命周期内不需要修改,不可变变量天然具备"线程安全""可预测性"的优势。
▶ 基本使用
⌨️
rust
// 默认不可变变量:定义后无法修改
let x = 5;
println!("x = {}", x); // 输出:x = 5
// 如果尝试修改,会直接编译错误:cannot assign twice to immutable variable `x`
// x = 6;
// 可变变量:用 `let mut` 声明
let mut y = 10;
println!("y = {}", y); // 输出:y = 10
y = 20;
println!("y = {}", y); // 输出:y = 20
💡 为什么默认不可变? 不可变变量是"线程安全"的充分条件------多个线程读取同一个不可变变量,永远不会出现数据竞争。这是Rust"无畏并发"的基础之一。
2.1.2 常量:比不可变变量更严格的"不变性"
常量用const声明,是编译时固定的"不可变值",它的规则比不可变变量更严格:
- 必须显式指定类型(不可省略);
- 只能用编译时可计算的表达式赋值(不能用运行时结果,比如函数返回值);
- 生命周期贯穿整个程序。
⌨️
rust
// 正确:编译时可计算的表达式
const MAX_SCORE: u32 = 100;
const PI: f64 = 3.1415926;
// 错误:用函数返回值赋值(运行时结果)
// const RANDOM: u32 = rand::random();
⚠️ 常量 vs 不可变变量 :不可变变量可以在运行时赋值(比如let x = get_user_input();),但常量不行;常量在编译时会被替换为实际值,而不可变变量会在栈上分配内存。
2.1.3 数据类型:标量与复合的分层设计
Rust的类型系统分为标量类型 (单个值)和复合类型(多个值组合),所有变量都必须有明确的类型(编译器可以自动推导,但本质是强类型语言)。
▶ 标量类型:最基础的"原子值"
- 整型 :表示整数,分有符号(
i8/i16/i32/i64,默认i32)和无符号(u8/u16/u32/u64); - 浮点型 :表示小数,分
f32(单精度)和f64(双精度,默认); - 布尔型 :只有
true和false(占用1字节); - 字符型 :用单引号
'表示,支持完整的UTF-8编码(包括中文、表情等)。
⌨️
rust
// 整型
let a: i32 = 10; // 有符号32位,默认
let b: u64 = 20; // 无符号64位
// 浮点型
let c = 3.14; // 默认f64
let d: f32 = 2.718;
// 布尔型
let is_true = true;
// 字符型:支持UTF-8
let ch1 = 'a';
let ch2 = '中';
let ch3 = '😀'; // 表情符号也能直接使用!
println!("{} {} {}", ch1, ch2, ch3); // 输出:a 中 😀
▶ 复合类型:组合多个值
Rust有两种基础复合类型:
- 元组(Tuple):固定长度,不同类型值的组合;
- 数组(Array):固定长度,相同类型值的组合。
💡 为什么都是固定长度? 因为Rust的栈内存大小在编译时必须确定------固定长度的类型才能在栈上分配。如果需要动态大小,必须用堆上的集合类型(比如Vec<T>)。
元组的使用
⌨️
rust
// 定义元组:(类型1, 类型2, ...)
let student = ("202401", "Alice", 95, 88);
// 类型自动推导为 (&str, &str, i32, i32)
// 方式1:用索引访问
println!("学号:{}", student.0); // 输出:202401
println!("数学成绩:{}", student.2); // 输出:95
// 方式2:模式匹配解构(Rust特色,优雅!)
let (id, name, math, chinese) = student;
println!("{} {} 数学:{} 语文:{}", id, name, math, chinese); // 输出完整信息
数组的使用
⌨️
rust
// 定义数组:[类型; 长度]
let scores = [95, 92, 88, 90];
// 类型自动推导为 [i32; 4]
// 安全访问:用 get() 方法返回 Option<&T>(避免越界panic)
match scores.get(5) {
Some(score) => println!("成绩:{}", score),
None => println!("无效索引"), // 越界时返回此信息,不会崩溃!
} // 输出:无效索引
// 不安全访问:直接用 [index],越界会panic(生产环境不推荐)
// println!("{}", scores[5]); // 运行时崩溃:index out of bounds
⚠️ Rust数组 vs 其他语言数组 :其他语言的数组大多是"动态长度"或"指针+长度",而Rust的数组是真正的"固定长度"------编译时会检查所有访问是否越界。
🔄 2.2 函数与流程控制:安全与优雅的逻辑表达
Rust的函数和流程控制设计兼顾了"类型安全"和"语法优雅" ,特别是match表达式和Option类型,是Rust"编译时错误检查"的核心武器。
2.2.1 函数:类型安全的定义与返回
Rust的函数用fn声明,规则明确:
- 参数必须显式指定类型;
- 返回值用
->声明,支持表达式返回 (省略return,函数最后一个表达式自动作为返回值)。
⌨️
rust
// 定义函数:参数a和b是i32类型,返回i32
fn add(a: i32, b: i32) -> i32 {
// 表达式返回:a + b 是函数最后一个表达式,自动作为返回值
a + b
}
fn main() {
let sum = add(5, 3);
println!("5 + 3 = {}", sum); // 输出:8
}
💡 为什么省略return? Rust的设计哲学是"少写冗余代码",表达式返回让函数更简洁。如果需要提前返回,可以用return(比如条件判断时)。
2.2.2 流程控制:从"语句"到"表达式"
Rust的流程控制大多是"表达式"而非"语句"------这意味着你可以把流程控制的结果直接赋值给变量。
▶ if表达式:条件赋值的优雅方式
⌨️
rust
let x = 10;
// if是表达式,直接赋值给result
let result = if x > 5 {
"大于5"
} else {
"小于等于5"
};
println!("result = {}", result); // 输出:大于5
💡 为什么是表达式? 因为Rust没有"三元运算符"(a ? b : c)------if表达式已经足够优雅地替代它。
▶ loop循环:可以返回值的无限循环
Rust的loop是无限循环 ,但它支持用break返回值------这是其他语言极少具备的特性。
⌨️
rust
let mut counter = 0;
// loop返回值赋值给result
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // 返回counter*2
}
};
println!("result = {}", result); // 输出:20
💡 使用场景 :需要重复尝试直到成功的场景(比如重试网络请求、解析数据),直接用loop返回成功结果,代码更简洁。
▶ for循环:遍历集合的"安全首选"
Rust的for循环自动遍历集合的所有元素,无需手动管理索引------从根本上避免了"数组越界"问题。
⌨️
rust
// 遍历Vec(动态数组)
let mut numbers = vec![1, 2, 3, 4];
// 不可变遍历:iter()返回不可变引用
for num in numbers.iter() {
println!("num = {}", num);
}
// 可变遍历:iter_mut()返回可变引用,修改元素值
for num in numbers.iter_mut() {
*num *= 2; // 注意:需要用*解引用
}
println!("修改后:{:?}", numbers); // 输出:[2, 4, 6, 8]
// 遍历范围:a..b是左闭右开,a..=b是闭区间
for i in 1..=5 {
println!("i = {}", i); // 输出:1-5
}
⚠️ 为什么不推荐while循环遍历集合? while循环需要手动管理索引(比如let mut i = 0; while i < len { ... i +=1 }),容易出现越界或逻辑错误,而for循环由编译器保证安全。
2.2.3 match表达式:Rust的"语法瑞士军刀"
match是Rust最强大的流程控制结构,它可以:
- 匹配值 、类型 、范围;
- 解构元组 、数组 、结构体;
- 穷尽性检查------必须覆盖所有可能的情况,否则编译错误!
这是Rust"编译时安全"的核心特性之一------永远不会出现"未处理的分支"。
▶ 范围匹配(替代其他语言的switch)
⌨️
rust
let score = 85;
// 范围匹配:90..=100 表示90到100(包括两端)
let grade = match score {
90..=100 => 'A',
80..=89 => 'B',
70..=79 => 'C',
0..=69 => 'D',
_ => '?', // 通配符,匹配所有其他情况,确保穷尽性
};
println!("成绩等级:{}", grade); // 输出:B
▶ 解构匹配
⌨️
rust
// 匹配元组
let point = (3, 4);
match point {
(0, 0) => println!("原点"),
(x, 0) => println!("x轴:{}", x),
(0, y) => println!("y轴:{}", y),
(x, y) => println!("点({},{})", x, y),
} // 输出:点(3,4)
// 匹配Option类型(空值处理,后面会讲)
let maybe_score = Some(95);
match maybe_score {
Some(s) => println!("成绩:{}", s),
None => println!("没有成绩"),
} // 输出:成绩:95
2.2.4 Option类型:安全的"空值替代方案"
其他语言的null是运行时错误的"万恶之源" (空指针解引用会导致程序崩溃)。Rust用Option<T>类型完全替代了null:
Option<T>有两种变体:Some(T)(有值)和None(空值);- 必须显式处理
None的情况,否则编译错误!
这是Rust"零空指针解引用"承诺的核心实现。
⌨️
rust
// 定义函数:查找数组中索引为index的元素,返回Option<&i32>
fn find_element(arr: &[i32], index: usize) -> Option<&i32> {
if index < arr.len() {
Some(&arr[index]) // 找到,返回Some(值)
} else {
None // 没找到,返回None
}
}
fn main() {
let scores = [95, 92, 88];
// 匹配返回结果,必须处理None
match find_element(&scores, 5) {
Some(score) => println!("找到成绩:{}", score),
None => println!("无效索引"), // 必须处理None
} // 输出:无效索引
// 简化处理:if let(只关心Some的情况)
if let Some(score) = find_element(&scores, 0) {
println!("第一个成绩:{}", score); // 输出:95
}
}
💡 为什么Option能保证安全? 因为Option<T>和T是不同的类型 ------你不能直接把Option<T>当作T使用,必须先"解包"(处理None的情况)。
📦 2.3 核心集合类型:堆上的数据结构
前面学的元组和数组都是栈上的固定长度类型 ,但实际开发中我们经常需要动态大小的数据结构。Rust的标准库提供了三种核心堆上集合:
Vec<T>:动态数组;String:动态字符串;HashMap<K, V>:键值对映射。
这些集合的所有权由变量持有,当变量离开作用域时,会自动释放堆上的内存------无需手动管理。
2.3.1 Vec:动态数组
Vec<T>是可变长度的数组,支持添加、删除、修改元素,适合存储"相同类型的序列数据"。
⌨️
rust
// 创建Vec的三种方式
let mut v1 = Vec::new(); // 空Vec
let mut v2 = vec![1, 2, 3]; // 用宏创建,自动推导类型
// 添加元素
v1.push(1);
v1.push(2);
v1.push(3);
// 安全访问:get()返回Option<&T>
match v2.get(2) {
Some(num) => println!("第三个元素:{}", num), // 输出:3
None => println!("无效索引"),
}
// 可变访问:get_mut()返回Option<&mut T>
if let Some(num) = v2.get_mut(0) {
*num = 10; // 修改第一个元素为10
}
println!("v2 = {:?}", v2); // 输出:[10, 2, 3]
// 遍历Vec
for num in v1.iter() {
println!("{}", num);
}
⚠️ Vec的内存管理 :当Vec的容量不足时,会自动扩容(通常是翻倍)。扩容时会分配新的堆内存,将旧数据拷贝到新内存------这会有一定的性能开销,但Rust的扩容策略已经过优化,实际使用中影响很小。
2.3.2 String:动态字符串
Rust的String是堆上分配的UTF-8编码字符串,支持拼接、切片、替换等操作,是Rust开发中最常用的字符串类型。
⌨️
rust
// 创建String的方式
let s1 = String::from("Hello"); // 从&str创建
let s2 = "World".to_string(); // 从字符串字面量创建
// 拼接字符串
let mut s3 = s1 + " " + &s2; // + 会转移s1的所有权,所以s2需要用&引用
println!("s3 = {}", s3); // 输出:Hello World
// 更灵活的拼接:用format!宏(不会转移所有权)
let s4 = String::from("Rust");
let s5 = format!("{} {}!", s3, s4);
println!("s5 = {}", s5); // 输出:Hello World Rust!
// 字符串切片:注意是按**字节**切片(不是字符!)
let s6 = String::from("你好,Rust");
let slice1 = &s6[0..3]; // 前3个字节 → "你"
let slice2 = &s6[4..7]; // 第4-6字节 → "好"
// let slice3 = &s6[0..4]; // 错误:跨UTF-8字符边界,运行时panic
// 正确的字符切片方式:用chars()迭代器
let slice3: String = s6.chars().take(2).collect();
println!("slice3 = {}", slice3); // 输出:你好
💡 为什么String切片是按字节? 因为Rust的String是UTF-8编码------每个字符的字节数不固定(中文是3字节)。如果按字符切片,需要遍历整个字符串计算字节边界,这会影响性能。Rust选择"显式性"而非"自动性",避免隐藏的性能开销。
2.3.3 HashMap<K, V>:键值对映射
HashMap<K, V>是哈希表 ,用于存储"键值对"数据,支持快速查找、插入、删除(时间复杂度O(1))。它位于std::collections模块中,需要手动导入。
⌨️
rust
use std::collections::HashMap;
// 创建HashMap
let mut scores = HashMap::new();
// 插入键值对
scores.insert(String::from("Alice"), 95);
scores.insert(String::from("Bob"), 92);
// 查找键对应的值
match scores.get(&String::from("Alice")) {
Some(score) => println!("Alice的成绩:{}", score), // 输出:95
None => println!("未找到"),
}
// 遍历HashMap
for (name, score) in &scores {
println!("{}: {}", name, score);
}
// 只在键不存在时插入
scores.entry(String::from("Charlie")).or_insert(88);
println!("Charlie的成绩:{}", scores.get(&String::from("Charlie")).unwrap()); // 输出:88
⚠️ HashMap的注意事项:
- 键类型必须实现
Hash和Eqtraits(Rust的基本类型都自动实现了); - 默认的哈希函数是SipHash (安全性高但性能中等),如果需要更高的性能,可以使用第三方库(比如
rustc-hash)。
💼 2.4 实战案例:学生成绩管理系统
现在我们用前面学习的所有核心语法,实现一个完整的学生成绩管理系统,功能包括:
- 添加学生(学号、姓名、数学/语文/英语成绩);
- 查看所有学生的成绩和平均分;
- 按学号查找学生;
- 按学号删除学生;
- 退出系统。
2.4.1 系统设计
- 数据结构 :用
Student结构体封装学生信息,HashMap<u32, Student>存储所有学生(学号作为唯一键); - 功能模块 :每个功能对应一个函数(
add_student/list_students/find_student/delete_student); - 输入输出 :用标准输入输出处理用户交互,用
Result类型处理输入错误; - 流程控制 :用
loop循环实现菜单,match匹配用户选择。
2.4.2 完整代码
⌨️
rust
use std::collections::HashMap;
use std::io::{self, BufRead, Write};
// 学生结构体:封装学号、姓名、三科成绩
struct Student {
id: u32, // 学号
name: String, // 姓名
math: u32, // 数学成绩
chinese: u32, // 语文成绩
english: u32, // 英语成绩
}
// 为Student实现方法:计算平均分
impl Student {
fn average(&self) -> f32 {
(self.math as f32 + self.chinese as f32 + self.english as f32) / 3.0
}
}
// 1. 添加学生
fn add_student(map: &mut HashMap<u32, Student>) -> io::Result<()> {
// 提示并读取学号
print!("请输入学号:");
io::stdout().flush()?; // 刷新输出,确保提示先显示
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let id: u32 = input.trim().parse()?; // 转换为u32
// 提示并读取姓名
print!("请输入姓名:");
io::stdout().flush()?;
input.clear();
io::stdin().read_line(&mut input)?;
let name = input.trim().to_string();
// 提示并读取数学成绩
print!("请输入数学成绩:");
io::stdout().flush()?;
input.clear();
io::stdin().read_line(&mut input)?;
let math: u32 = input.trim().parse()?;
// 提示并读取语文成绩
print!("请输入语文成绩:");
io::stdout().flush()?;
input.clear();
io::stdin().read_line(&mut input)?;
let chinese: u32 = input.trim().parse()?;
// 提示并读取英语成绩
print!("请输入英语成绩:");
io::stdout().flush()?;
input.clear();
io::stdin().read_line(&mut input)?;
let english: u32 = input.trim().parse()?;
// 创建学生对象并插入HashMap
let student = Student { id, name, math, chinese, english };
if map.insert(id, student).is_some() {
println!("警告:学号 {} 已存在,已更新信息", id);
} else {
println!("学生信息添加成功");
}
Ok(())
}
// 2. 查看所有学生
fn list_students(map: &HashMap<u32, Student>) {
if map.is_empty() {
println!("暂无学生信息");
return;
}
// 打印表头
println!("{}", "=".repeat(65));
println!("{:>10} {:<12} {:>8} {:>8} {:>8} {:>12}", "学号", "姓名", "数学", "语文", "英语", "平均分");
println!("{}", "-".repeat(65));
// 遍历并打印学生信息
for (_, student) in map {
println!(
"{:>10} {:<12} {:>8} {:>8} {:>8} {:>12.2}",
student.id,
student.name,
student.math,
student.chinese,
student.english,
student.average()
);
}
println!("{}", "=".repeat(65));
}
// 3. 按学号查找学生
fn find_student(map: &HashMap<u32, Student>) -> io::Result<()> {
print!("请输入学号:");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let id: u32 = input.trim().parse()?;
match map.get(&id) {
Some(student) => {
println!("{}", "=".repeat(50));
println!("学号:{}", student.id);
println!("姓名:{}", student.name);
println!("数学成绩:{}", student.math);
println!("语文成绩:{}", student.chinese);
println!("英语成绩:{}", student.english);
println!("平均分:{:.2}", student.average());
println!("{}", "=".repeat(50));
}
None => println!("未找到学号为 {} 的学生", id),
}
Ok(())
}
// 4. 按学号删除学生
fn delete_student(map: &mut HashMap<u32, Student>) -> io::Result<()> {
print!("请输入学号:");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let id: u32 = input.trim().parse()?;
if map.remove(&id).is_some() {
println!("学号 {} 的学生信息已删除", id);
} else {
println!("未找到学号为 {} 的学生", id);
}
Ok(())
}
// 主函数:系统入口
fn main() -> io::Result<()> {
// 初始化学生信息HashMap
let mut student_map: HashMap<u32, Student> = HashMap::new();
// 菜单循环
loop {
// 打印菜单
println!("\n=== 学生成绩管理系统 ===");
println!("1. 添加学生");
println!("2. 查看所有学生");
println!("3. 按学号查找学生");
println!("4. 按学号删除学生");
println!("5. 退出系统");
print!("请选择操作(1-5):");
io::stdout().flush()?;
// 读取用户选择
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let choice: u32 = match input.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("无效输入,请输入1-5的数字");
continue; // 重新循环
}
};
// 匹配选择并执行对应功能
match choice {
1 => add_student(&mut student_map)?,
2 => list_students(&student_map),
3 => find_student(&student_map)?,
4 => delete_student(&mut student_map)?,
5 => {
println!("退出系统,感谢使用!");
break; // 退出循环
}
_ => println!("无效选择,请输入1-5的数字"),
}
}
Ok(())
}
2.4.3 运行与测试
- 编译运行 :在项目目录下执行
cargo run; - 测试流程 :
① 选择1添加学生:学号202401,姓名Alice,数学95,语文92,英语88;
② 选择2查看所有学生:显示Alice的信息和平均分91.67;
③ 选择3查找学号202401:显示详细信息;
④ 选择4删除学号202401:提示删除成功;
⑤ 选择5退出系统。
2.4.4 代码亮点
- 结构体封装 :用
Student结构体清晰地封装了学生信息; - 方法实现 :为
Student实现average方法,符合"面向对象"的封装思想; - 错误处理 :所有I/O操作都用
Result类型处理,避免运行时崩溃; - 安全访问 :HashMap的
get/insert/remove方法都经过编译器安全检查; - 用户体验 :用
io::stdout().flush()确保提示信息先显示,避免用户输入混乱。
✅ 总结
这一篇我们系统学习了Rust的核心语法,包括:
- 变量与数据类型:默认不可变的设计哲学,标量与复合类型的安全使用;
- 函数与流程控制 :类型安全的函数定义,
if/loop/for的优雅表达,match表达式的强大功能,Option类型的安全空值处理; - 核心集合类型 :
Vec<T>/String/HashMap<K, V>的堆上内存管理与安全访问; - 实战案例:用所有核心知识点实现了一个完整的学生成绩管理系统,展示了Rust语法的"安全与优雅"。
Rust的语法设计处处体现了"编译时安全"的哲学 ------从默认不可变变量到match的穷尽性检查,从Option的空值处理到集合的安全访问,这些设计都把"运行时错误"提前到"编译时解决",让你写出"无畏崩溃"的代码。
下一篇我们会深入学习Rust的所有权、借用、生命周期------这是Rust的"灵魂",也是写出真正"零内存安全问题"代码的关键。