Skip to content

Rust 系统学习:从基础语法到工程实践 · 用模块和测试完成一个小库至完成基础之后 ​

返回分篇目录

用模块和测试完成一个小库 ​

Package、crate 与 module ​

Cargo package 由 Cargo.toml 描述,可以包含库和二进制等目标。crate 是编译单元;默认的 src/lib.rs 是库 crate 根,src/main.rs 是二进制 crate 根。module 在 crate 内组织代码与可见性,不等于另一个 package。

mod scores; 声明并加载模块,use 把路径引入当前作用域;use 不会替你加载一个未声明的源文件模块。crate:: 从当前 crate 根开始,self:: 从当前模块开始,super:: 指向父模块。子模块可以访问祖先模块的私有项;对外提供接口才需要适当的 pub / pub(crate)。参见路径与可见性。

从一个有失败路径的函数开始 ​

运行 cargo new scorebook --lib,把 src/lib.rs 替换为以下内容。它只做一件事:解析一个 0 到 100 的分数,并让调用者区分格式错误和越界。

rust
#[derive(Debug, PartialEq)]
pub enum ScoreError {
    InvalidNumber,
    OutOfRange,
}

pub fn parse_score(text: &str) -> Result<u8, ScoreError> {
    let score = text.trim().parse::<u16>()
        .map_err(|_| ScoreError::InvalidNumber)?;
    if score > 100 {
        return Err(ScoreError::OutOfRange);
    }
    Ok(score as u8) // 前面已验证范围
}

#[cfg(test)]
mod tests {
    use super::{parse_score, ScoreError};

    #[test]
    fn accepts_boundaries_and_whitespace() {
        assert_eq!(parse_score("0"), Ok(0));
        assert_eq!(parse_score(" 100 "), Ok(100));
    }

    #[test]
    fn rejects_invalid_number_and_out_of_range() {
        assert_eq!(parse_score("nope"), Err(ScoreError::InvalidNumber));
        assert_eq!(parse_score("101"), Err(ScoreError::OutOfRange));
    }
}

这个接口把无法解析为 u16 的输入(包括负数、超过 u16 范围的数)归为 InvalidNumber;能解析但超过 100 的归为 OutOfRange。如果业务要求所有数值范围错误都归为同类,需要进一步设计解析规则,不能仅修改测试期望。

运行 cargo test,预期两个测试通过;格式与静态检查按工具链页的检查顺序执行。#[cfg(test)] 让测试模块只在测试构建中加入;#[test] 标记要执行的测试。测试验证具体行为,不能替代对输入规则的定义。参见编写测试。

后续练习:把 parse_score 移到 src/scores.rs,在 src/lib.rs 写 mod scores; pub use scores::{parse_score, ScoreError};,保持外部接口与测试结果一致。然后再增加一个 tests/score_api.rs 集成测试,从 scorebook::parse_score 验证公开接口。

完成基础之后 ​

练习提示与参考解法

先独立预测类型、所有权和边界输出,再展开核对;下面只复用正文已经介绍的语法。

  • 分数练习可先定义 0..=59 => "retry"、60..=100 => "pass",其余 u8 值返回 "invalid";59/60分别为retry/pass,101为invalid。题目若增加等级,应相应细分区间。
  • 将 word_count 改为获取 String 时,调用 word_count(message) 后再使用 message 应报移动错误;读取接口恢复为 &str 并传 &message,便可在读取结束后修改原值。
  • 给 Task 实现 Summary 的核心是 impl Summary for Task { fn summary(&self) -> &str { self.title() } }。它与 Note 共用 print_summary,返回借用随 &self 有效,无需克隆标题。将这些定义放在同一个完整示例里,避免重复定义 main。
  • match average(input) 需覆盖 Ok(Some(value))、Ok(None)、Err(error),分别表示有平均数、空输入、解析失败;可用 assert!(average("4294967296").is_err()) 检查超出u32范围的输入。
  • 模块练习先把原公开类型、函数和单元测试一起移入 src/scores.rs,再在 src/lib.rs 声明与再导出。tests/score_api.rs 的完整最小文件可写为:
rust
use scorebook::{parse_score, ScoreError};

#[test]
fn public_score_api() {
    assert_eq!(parse_score("60"), Ok(60));
    assert_eq!(parse_score("101"), Err(ScoreError::OutOfRange));
}

未来执行 cargo test 时应保留原两条单元测试并增加这条集成测试;本轮仅静态审读,未执行示例或测试。

先能独立写完上述小库,再按实际需求学习线程与 Send / Sync、智能指针、异步编程、FFI 或 unsafe。旧译文中的编译器插件、旧 asm!、box 语法、语言项和 feature gate 示例有历史背景,不应作为现行稳定 Rust 的起步步骤。

日常开发的命令、依赖更新与诊断顺序见工具链手册。持续学习可使用现行官方 Book、标准库 API及Cargo Book。

历史对照资料见第一版译文系列。

继续阅读