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 的分数,并让调用者区分格式错误和越界。
#[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的完整最小文件可写为:
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。
历史对照资料见第一版译文系列。
继续阅读
- 排错
- 排错
- 排错