变量与数据类型:从“默认不可变”说起

变量与数据类型:从"默认不可变"说起

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声明,是编译时固定的"不可变值",它的规则比不可变变量更严格:

  1. 必须显式指定类型(不可省略);
  2. 只能用编译时可计算的表达式赋值(不能用运行时结果,比如函数返回值);
  3. 生命周期贯穿整个程序。

⌨️

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(双精度,默认);
  • 布尔型 :只有truefalse(占用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有两种基础复合类型:

  1. 元组(Tuple):固定长度,不同类型值的组合;
  2. 数组(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声明,规则明确:

  1. 参数必须显式指定类型
  2. 返回值用->声明,支持表达式返回 (省略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的标准库提供了三种核心堆上集合:

  1. Vec<T>:动态数组;
  2. String:动态字符串;
  3. 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的注意事项

  • 键类型必须实现HashEq traits(Rust的基本类型都自动实现了);
  • 默认的哈希函数是SipHash (安全性高但性能中等),如果需要更高的性能,可以使用第三方库(比如rustc-hash)。

💼 2.4 实战案例:学生成绩管理系统

现在我们用前面学习的所有核心语法,实现一个完整的学生成绩管理系统,功能包括:

  1. 添加学生(学号、姓名、数学/语文/英语成绩);
  2. 查看所有学生的成绩和平均分;
  3. 按学号查找学生;
  4. 按学号删除学生;
  5. 退出系统。

2.4.1 系统设计

  1. 数据结构 :用Student结构体封装学生信息,HashMap<u32, Student>存储所有学生(学号作为唯一键);
  2. 功能模块 :每个功能对应一个函数(add_student/list_students/find_student/delete_student);
  3. 输入输出 :用标准输入输出处理用户交互,用Result类型处理输入错误;
  4. 流程控制 :用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 运行与测试

  1. 编译运行 :在项目目录下执行cargo run
  2. 测试流程
    ① 选择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的核心语法,包括:

  1. 变量与数据类型:默认不可变的设计哲学,标量与复合类型的安全使用;
  2. 函数与流程控制 :类型安全的函数定义,if/loop/for的优雅表达,match表达式的强大功能,Option类型的安全空值处理;
  3. 核心集合类型Vec<T>/String/HashMap<K, V>的堆上内存管理与安全访问;
  4. 实战案例:用所有核心知识点实现了一个完整的学生成绩管理系统,展示了Rust语法的"安全与优雅"。

Rust的语法设计处处体现了"编译时安全"的哲学 ------从默认不可变变量到match的穷尽性检查,从Option的空值处理到集合的安全访问,这些设计都把"运行时错误"提前到"编译时解决",让你写出"无畏崩溃"的代码。

下一篇我们会深入学习Rust的所有权、借用、生命周期------这是Rust的"灵魂",也是写出真正"零内存安全问题"代码的关键。

相关推荐
Scott9999HH7 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社8 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海8 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖9 小时前
JDK 26 新特性详解
java·开发语言
马优晨9 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区11 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大12 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai12 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜12 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa12 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio