第12篇:模块与错误处理——组织代码与处理问题

作者: 李金雨
联系方式: wbtm2718@qq.com
目标读者: Rust中文编程
核心理念: AI时代必须使用中文编程,母语编程阅读效率极高


1. 开篇引入

本课目标

  • 理解什么是模块
  • 掌握模块的创建和使用方法
  • 理解模块的访问控制
  • 掌握错误处理的基本方法
  • 理解Result类型的使用
  • 掌握错误传播(?操作符)的使用

生活场景引入

想象一下,你有一个工具箱,里面有很多工具:

  • 锤子和螺丝刀放在一个格子里
  • 尺子和铅笔放在另一个格子里
  • 剪刀和胶水放在第三个格子里

这样,你可以很容易地找到你需要的工具。在编程中,模块就像工具箱的格子,它可以帮助你组织代码,使代码更加清晰。

另外,当你使用工具时,可能会遇到一些问题,比如锤子坏了,或者螺丝刀找不到了。在编程中,错误处理就像处理这些问题,它可以帮助你优雅地处理程序运行时的错误。

预期成果展示

通过这节课的学习,你将能够:

  • 创建和使用模块来组织代码
  • 理解模块的访问控制
  • 使用Result类型处理错误
  • 使用?操作符传播错误
  • 编写更加健壮的程序

2. 概念讲解

模块系统

什么是模块

模块是Rust中组织代码的方式,它可以将相关的代码放在一起,使代码更加清晰和可维护。

模块的创建

在Rust中,我们使用mod关键字来创建模块:

fn main() {
    // 定义一个模块
    mod 数学工具 {
        pub fn 加法(a: i32, b: i32) -> i32 {
            a + b
        }

        pub fn 减法(a: i32, b: i32) -> i32 {
            a - b
        }
    }
}

模块的访问控制

模块中的项默认是私有的,只有模块内部可以访问。如果要让模块外部也能访问,需要使用pub关键字:

fn main() {
    mod 数学工具 {
        pub fn 加法(a: i32, b: i32) -> i32 { // 公共函数
            a + b
        }

        fn 减法(a: i32, b: i32) -> i32 { // 私有函数
            a - b
        }
    }

    println!("1 + 2 = {}", 数学工具::加法(1, 2)); // 正确:加法是公共的
    // println!("3 - 1 = {}", 数学工具::减法(3, 1)); // 错误:减法是私有的
}

模块的嵌套

模块可以嵌套,形成层次结构:

fn main() {
    mod 工具包 {
        pub mod 数学工具 {
            pub fn 加法(a: i32, b: i32) -> i32 {
                a + b
            }
        }

        pub mod 字符串工具 {
            pub fn 反转(文本: &str) -> String {
                文本.chars().rev().collect()
            }
        }
    }

    println!("1 + 2 = {}", 工具包::数学工具::加法(1, 2));
    println!("反转:{}", 工具包::字符串工具::反转("你好"));
}

使用use关键字

使用use关键字可以简化模块的使用:

fn main() {
    mod 工具包 {
        pub mod 数学工具 {
            pub fn 加法(a: i32, b: i32) -> i32 {
                a + b
            }
        }
    }

    use 工具包::数学工具::加法;

    println!("1 + 2 = {}", 加法(1, 2)); // 直接使用加法,不需要前缀
}

错误处理

Result类型

Rust使用Result<T, E>类型来处理错误,其中T是成功时的类型,E是错误时的类型:

fn main() {
    enum Result<T, E> {
        Ok(T), // 成功,包含成功值
        Err(E), // 失败,包含错误值
    }
}

例如:

fn main() {
    fn 除法(分子: i32, 分母: i32) -> Result<i32, String> {
        if 分母 == 0 {
            Err(String::from("除数不能为零"))
        } else {
            Ok(分子 / 分母)
        }
    }

    match 除法(10, 2) {
        Ok(结果) => println!("结果:{}", 结果),
        Err(错误) => println!("错误:{}", 错误),
    }

    match 除法(10, 0) {
        Ok(结果) => println!("结果:{}", 结果),
        Err(错误) => println!("错误:{}", 错误),
    }
}

错误传播(?操作符)

?操作符可以简化错误处理,它会自动将错误传播给调用者:

fn main() {
    fn 读取文件() -> Result<String, std::io::Error> {
        let 内容 = std::fs::read_to_string("文件.txt")?; // 如果出错,自动返回错误
        Ok(内容)
    }

    match 读取文件() {
        Ok(内容) => println!("文件内容:{}", 内容),
        Err(错误) => println!("错误:{}", 错误),
    }
}

自定义错误类型

你可以定义自己的错误类型:

fn main() {
    #[derive(Debug)]
    enum 计算错误 {
        除数为零,
        负数平方根,
    }

    fn 除法(分子: i32, 分母: i32) -> Result<i32, 计算错误> {
        if 分母 == 0 {
            Err(计算错误::除数为零)
        } else {
            Ok(分子 / 分母)
        }
    }

    fn 平方根(数字: f64) -> Result<f64, 计算错误> {
        if 数字 < 0.0 {
            Err(计算错误::负数平方根)
        } else {
            Ok(数字.sqrt())
        }
    }

    match 除法(10, 0) {
        Ok(结果) => println!("结果:{}", 结果),
        Err(错误) => println!("错误:{:?}", 错误),
    }

    match 平方根(-1.0) {
        Ok(结果) => println!("结果:{}", 结果),
        Err(错误) => println!("错误:{:?}", 错误),
    }
}

panic!宏

panic!宏用于处理不可恢复的错误,它会立即终止程序:

fn main() {
    let 年龄 = -5;
    if 年龄 < 0 {
        panic!("年龄不能为负数");
    }
    println!("年龄:{}", 年龄);
}

3. 动手实践

基础练习(必做)

  1. 创建"学生管理"模块

    mod 学生管理 {
        pub struct 学生 {
            pub 姓名: String,
            pub 年龄: i32,
            pub 成绩: i32,
        }
        
        pub fn 添加学生(学生们: &mut Vec<学生>, 学生: 学生) {
            学生们.push(学生);
        }
        
        pub fn 删除学生(学生们: &mut Vec<学生>, 索引: usize) -> Option<学生> {
            if 索引 < 学生们.len() {
                Some(学生们.remove(索引))
            } else {
                None
            }
        }
        
        pub fn 查询学生(学生们: &Vec<学生>, 姓名: &str) -> Option<&学生> {
            学生们.iter().find(|学生| 学生.姓名 == 姓名)
        }
    }
    
    fn main() {
        use 学生管理::{学生, 添加学生, 删除学生, 查询学生};
        
        let mut 学生们 = vec![];
        
        // 添加学生
        添加学生(&mut 学生们, 学生 { 姓名: String::from("张三"), 年龄: 15, 成绩: 95 });
        添加学生(&mut 学生们, 学生 { 姓名: String::from("李四"), 年龄: 14, 成绩: 88 });
        
        // 查询学生
        if let Some(学生) = 查询学生(&学生们, "张三") {
            println!("找到学生:{},年龄:{}岁,成绩:{}分", 学生.姓名, 学生.年龄, 学生.成绩);
        }
        
        // 删除学生
        if let Some(学生) = 删除学生(&mut 学生们, 0) {
            println!("删除学生:{}", 学生.姓名);
        }
        
        // 打印剩余学生
        println!("剩余学生:");
        for 学生 in &学生们 {
            println!("{},{}岁,{}分", 学生.姓名, 学生.年龄, 学生.成绩);
        }
    }
    
  2. 创建"文件操作"模块,处理文件读写错误

    mod 文件操作 {
        use std::fs;
        use std::io;
        
        pub fn 读取文件(路径: &str) -> Result<String, io::Error> {
            fs::read_to_string(路径)
        }
        
        pub fn 写入文件(路径: &str, 内容: &str) -> Result<(), io::Error> {
            fs::write(路径, 内容)
        }
    }
    
    fn main() {
        use 文件操作::{读取文件, 写入文件};
        
        // 写入文件
        match 写入文件("测试.txt", "你好,世界!") {
            Ok(_) => println!("文件写入成功"),
            Err(错误) => println!("写入错误:{}", 错误),
        }
        
        // 读取文件
        match 读取文件("测试.txt") {
            Ok(内容) => println!("文件内容:{}", 内容),
            Err(错误) => println!("读取错误:{}", 错误),
        }
        
        // 读取不存在的文件
        match 读取文件("不存在的文件.txt") {
            Ok(内容) => println!("文件内容:{}", 内容),
            Err(错误) => println!("读取错误:{}", 错误),
        }
    }
    
  3. 创建"计算器"模块,处理各种计算错误

    mod 计算器 {
        #[derive(Debug)]
        pub enum 计算错误 {
            除数为零,
            负数平方根,
        }
        
        pub fn 除法(分子: f64, 分母: f64) -> Result<f64, 计算错误> {
            if 分母 == 0.0 {
                Err(计算错误::除数为零)
            } else {
                Ok(分子 / 分母)
            }
        }
        
        pub fn 平方根(数字: f64) -> Result<f64, 计算错误> {
            if 数字 < 0.0 {
                Err(计算错误::负数平方根)
            } else {
                Ok(数字.sqrt())
            }
        }
    }
    
    fn main() {
        use 计算器::{除法, 平方根, 计算错误};
        
        // 测试除法
        match 除法(10.0, 2.0) {
            Ok(结果) => println!("10.0 / 2.0 = {}", 结果),
            Err(错误) => println!("除法错误:{:?}", 错误),
        }
        
        match 除法(10.0, 0.0) {
            Ok(结果) => println!("10.0 / 0.0 = {}", 结果),
            Err(错误) => println!("除法错误:{:?}", 错误),
        }
        
        // 测试平方根
        match 平方根(9.0) {
            Ok(结果) => println!("√9.0 = {}", 结果),
            Err(错误) => println!("平方根错误:{:?}", 错误),
        }
        
        match 平方根(-1.0) {
            Ok(结果) => println!("√-1.0 = {}", 结果),
            Err(错误) => println!("平方根错误:{:?}", 错误),
        }
    }
    

进阶练习(选做)

  1. 使用?操作符处理错误

    mod 文件操作 {
        use std::fs;
        use std::io;
        
        pub fn 读取并处理文件(路径: &str) -> Result<String, io::Error> {
            let 内容 = fs::read_to_string(路径)?; // 错误传播
            let 处理后的内容 = 内容.to_uppercase();
            Ok(处理后的内容)
        }
    }
    
    fn main() {
        use 文件操作::读取并处理文件;
        
        match 读取并处理文件("测试.txt") {
            Ok(内容) => println!("处理后的内容:{}", 内容),
            Err(错误) => println!("错误:{}", 错误),
        }
    }
    
  2. 创建嵌套模块

    mod 工具包 {
        pub mod 数学工具 {
            pub fn 加法(a: i32, b: i32) -> i32 {
                a + b
            }
            
            pub fn 减法(a: i32, b: i32) -> i32 {
                a - b
            }
        }
        
        pub mod 字符串工具 {
            pub fn 反转(文本: &str) -> String {
                文本.chars().rev().collect()
            }
            
            pub fn 统计字数(文本: &str) -> usize {
                文本.chars().count()
            }
        }
    }
    
    fn main() {
        use 工具包::数学工具::{加法, 减法};
        use 工具包::字符串工具::{反转, 统计字数};
        
        println!("1 + 2 = {}", 加法(1, 2));
        println!("5 - 3 = {}", 减法(5, 3));
        println!("反转:{}", 反转("你好"));
        println!("字数:{}", 统计字数("你好,世界!"));
    }
    

挑战练习(拓展)

  1. 创建一个完整的模块系统
    // 主程序
    mod 学生管理 {
        pub mod 数据 {
            pub struct 学生 {
                pub 姓名: String,
                pub 年龄: i32,
                pub 成绩: i32,
            }
        }
        
        pub mod 操作 {
            use super::数据::学生;
            
            pub fn 添加学生(学生们: &mut Vec<学生>, 学生: 学生) {
                学生们.push(学生);
            }
            
            pub fn 计算平均成绩(学生们: &Vec<学生>) -> f64 {
                if 学生们.is_empty() {
                    return 0.0;
                }
                
                let mut 总分 = 0;
                for 学生 in 学生们 {
                    总分 += 学生.成绩;
                }
                
                总分 as f64 / 学生们.len() as f64
            }
        }
    }
    
    fn main() {
        use 学生管理::数据::学生;
        use 学生管理::操作::{添加学生, 计算平均成绩};
        
        let mut 学生们 = vec![];
        
        添加学生(&mut 学生们, 学生 { 姓名: String::from("张三"), 年龄: 15, 成绩: 95 });
        添加学生(&mut 学生们, 学生 { 姓名: String::from("李四"), 年龄: 14, 成绩: 88 });
        添加学生(&mut 学生们, 学生 { 姓名: String::from("王五"), 年龄: 15, 成绩: 92 });
        
        println!("学生列表:");
        for 学生 in &学生们 {
            println!("{},{}岁,{}分", 学生.姓名, 学生.年龄, 学生.成绩);
        }
        
        let 平均分 = 计算平均成绩(&学生们);
        println!("平均成绩:{}分", 平均分);
    }
    

4. 知识总结

核心概念回顾

  • 模块:组织代码的方式,将相关的代码放在一起
  • 访问控制:使用pub关键字控制模块项的可见性
  • 模块嵌套:模块可以嵌套,形成层次结构
  • use关键字:简化模块的使用
  • 错误处理:处理程序运行时的错误
  • Result类型:表示可能失败的操作
  • ?操作符:简化错误传播
  • 自定义错误类型:定义自己的错误类型
  • panic!宏:处理不可恢复的错误

关键代码速查

功能代码说明
模块定义mod 模块名 { 代码 }定义一个模块
公共项pub fn 函数名() {}定义一个公共函数
模块嵌套mod 模块名 { mod 子模块 { 代码 } }定义嵌套模块
使用模块use 模块名::项;简化模块的使用
Result类型fn 函数() -> Result<T, E> { 代码 }定义可能失败的函数
错误传播let 结果 = 可能失败的操作()?;自动传播错误
错误处理match 结果 { Ok(值) => 处理成功, Err(错误) => 处理错误 }处理Result类型

常见错误提醒

  1. 访问控制:忘记使用pub关键字,导致外部无法访问模块项
  2. 模块路径:模块路径错误,导致无法找到模块项
  3. 错误处理:忘记处理Result类型,导致编译错误
  4. ?操作符:在非Result返回类型的函数中使用?操作符
  5. panic!:过度使用panic!,应该使用Result类型处理可恢复的错误

5. 课后作业

巩固练习题

  1. 编写一个程序,创建一个"数学工具"模块,包含加减乘除函数
  2. 编写一个程序,创建一个"字符串工具"模块,包含字符串处理函数
  3. 编写一个程序,使用Result类型处理文件读写错误

创意编程题

  1. 发挥你的创意,创建一个完整的模块系统,包含多个子模块
  2. 编写一个程序,实现一个简单的错误处理系统,处理各种可能的错误

下篇预习提示

下一篇我们将学习"综合实战——制作我的小游戏",这是一个综合运用所学知识的项目。预习一下:

  • 如何选择一个适合的项目?
  • 如何设计项目的结构?
  • 如何实现项目的功能?
  • 如何测试和调试项目?

恭喜你完成了第十二篇教程!你已经掌握了模块系统和错误处理,学会了如何组织代码和处理问题。继续加油,下一篇我们将进行综合实战,制作一个完整的小游戏!

学习交流

如果你在学习过程中遇到任何问题,欢迎联系我:

  • ** 视频号,“时空系”
  • 邮箱: wbtm2718@qq.com
  • 其他提示:
  • 本人正在开发纯中文编程语言和编译器,有兴趣的可以交流
  • 本人正在写一本基于中国母语思维习惯的数学教材,可以让你一年内学完从初中到研究生的数学,而且可以让你快速掌握人工智能的关键数学知识,有兴趣的可以交流

祝你学习愉快!

更多推荐