介绍
一个野外小组需要两支能正常工作的手电筒。两行扫描记录可能对应两支不同的手电筒、同一支手电筒扫描了两次,或者一支完好、一支损坏。程序必须保留这些区别,才能判断装备是否齐全。
在本实验中,你将用 Rust 实现状态解析器和资产收集器。你会保留不同的实物资产、忽略内容相同的重复扫描,并在不修改源文件的前提下拒绝矛盾记录。下一个实验将利用这些身份,解释每种装备的缺口或富余。这些是合成记录,不涉及对实体装备的检查。
你应已了解 Rust 结构体、枚举、HashMap、Result、文本文件读取和基本的 for 循环。预备好的解析器、命令接口和输出格式化器,让你专注于身份判断。作者估计需要 45 分钟,包含阅读和普通纠错;这一时长未经初学者实际计时验证。
准确读取状态
本步骤检查两个真实扫描文件,预测其中的可用物品,并实现简短的状态判断,让程序正常的逐行视图能够工作。打开 Terminal。准备好的项目位于 /home/labex/project/field-kit,使用 Rust 1.99.1。
下面的 export 将 Cargo 目录加入当前 Shell 的命令搜索路径。$PATH 保留已有目录。这只对当前 Terminal 会话生效;新 Shell 中需重新执行。它不会编辑 Shell 启动文件。
export PATH=/home/labex/.cargo/bin:$PATH
用 cd 选择项目;如果丢失当前位置,再执行一次。pwd 打印当前目录,便于确认后续文件编辑发生在哪里。
cd /home/labex/project/field-kit
pwd
应显示 /home/labex/project/field-kit。路径拼错时用 cd 修正,不要重置 VM。cat 打印文件,不修改它。阅读这两个实际输入:
cat data/two-good.tsv
cat data/one-damaged.tsv
TSV 表示制表符分隔值。每条扫描有三个字段:asset_id、type_id、condition,以真正的制表符分隔。第一个文件中,实物资产 a 和 b 的状态均为 good。第二个文件中,a 完好,b 为 damaged。手动核对结果:两个文件都有两件实物,但只有第一个描述了两支可用手电筒。good 和 damaged 是两个准确的允许状态词;损坏装备是合法数据,不是错误输入行。
项目提供包含结构体与 TSV 读取器的 src/lib.rs、包含命令接口和格式化器的 src/main.rs,以及后续实验使用的 src/readiness.rs。保持这些文件、Cargo.toml、rust-toolchain.toml 和全部原始 data/ 文件不变。这里仅可编辑 src/condition.rs,下一步可编辑 src/identity.rs,并保留其公开名称和类型。
阅读状态模块:
cat src/condition.rs
枚举和显示方法已经提供。impl 将 Condition 的方法组织在一起;Self 在该代码块中指这个枚举。pub 将名称公开给读取器。derive 提供复制、调试显示和相等比较。parse 返回 Result<Self, String>:要么是带状态的 Ok,要么是带有自有错误说明的 Err。当前占位实现拒绝所有词,因此还不能区分这两个正常文件。
编辑前建立一个仅含源码的恢复点。mktemp -d 将六个 X 替换为唯一后缀来创建新目录,不会复用旧备份。$() 将打印的路径存入会话变量。&& 只在创建目录成功后运行 cp -p;该复制保留源码元数据。引号让变量中的路径保持完整。任一操作失败时,阅读错误,成功取得备份后再编辑:
rs03_before_condition=$(mktemp -d .edit-before-condition.XXXXXX) && cp -p src/condition.rs "$rs03_before_condition/condition.rs"
用 nano 打开已有模块;-c 显示光标位置:
nano -c src/condition.rs
nano 的 ^ 表示 Ctrl。按 Ctrl+W,输入 pub fn parse,按 Enter 定位函数。阅读其起止花括号。用方向键和 Backspace/Delete 只编辑此方法的函数体及未使用参数的绑定名称。将 _value 改为 value,保留参数类型。保留枚举、derive 和 name 方法。
编写 match value 表达式,为每个允许词设置精确分支,并用通配分支处理其余字符串。"good" => Ok(Self::Good), 这样的分支展示语法。自行构造对应的 Damaged 结果。_ 匹配其余值,必须返回 Err,不能默默编造状态。format! 创建 String;{value:?} 给意外文本加上引号,让空格和拼写在诊断中可见。用它说明输入必须是 good 或 damaged。闭合 match 和函数的花括号,让 match 保持为方法的返回结果。
按 Ctrl+O,再按 Enter 确认 src/condition.rs;Ctrl+X 退出 nano。不要仍在编辑器中时输入 Shell 命令。阅读保存后的编辑:
cat src/condition.rs
cargo run 编译并启动准备好的程序。--quiet 隐藏 Cargo 进度,-- 将其后参数传给程序。rows 验证并显示实际文件的每一行,尚不去重。
cargo run --quiet -- rows data/two-good.tsv
asset_id type_id condition
a light-a good
b light-a good
正常的逐行视图现在应显示两个 ID 及其完好状态。与另一个源文件比较:
cargo run --quiet -- rows data/one-damaged.tsv
asset_id type_id condition
a light-a good
b light-a damaged
这与你的手动预测一致:仍然扫描了两件物品,只有一件完好。你在实际读取器路径中写入了真实判断,不是另写一个演示程序。
受控输入契约
也阅读实际计划:
cat data/plan.tsv
字段为 type_id、name、required。light-a 和 light-b 都显示为 Flashlight,但类型 ID 不同。名称是显示文本,不是身份。正需求量没有扫描记录时存在缺口;零需求量没有扫描记录时缺口为零。提供的读取器采用以下规则:
- ID 以小写 ASCII 字母开头,后续只含小写字母、数字或
-。区分大小写。 - 状态必须准确为
good和damaged;不接受其他拼写。 - 名称是非空可打印 ASCII,可含空格。数量是值为 0~999 的十进制数字,允许前导零。计划类型 ID 不得重复,即使内容完全相同也不行。
- 准确的三字段表头必须在首行。允许 LF/CRLF、缺少最终换行,以及后续完全空的行。仅有空白字符的行不算空行。单独的回车符不是行结束符。字段不去除首尾空白:ID 或状态中的空格是错误,名称中的空格则保留。
这里的 ASCII 指基本英文字母、数字、空格和标点;名称排除控制字符。这是一个小型明确格式,不是通用 CSV 读取器。你的状态方法在准备好的读取器内提供两状态判断。
恢复源码编辑
遇到 Rust 错误时,阅读 Cargo 给出的文件和行号,重新打开该文件,搜索相关函数并修复编辑。保存、退出,再重试失败的命令。若要丢弃尚未保存的误编辑,按 Ctrl+X,在保存询问处按 N。这不会撤销之前已保存的修改。
仅在恢复已保存的错误时,恢复自己的状态检查点,不要重置 setup。在同一个 Terminal 中,变量仍指向备份。在新 Terminal 中,重新执行 PATH 和 cd,再列出本阶段的实际目录:
ls -d .edit-before-condition.*
-d 列出目录名而非内容;* 匹配生成的后缀。运行下列命令,在 Shell 等待时粘贴一个准确打印出的目录名,再按 Enter。read 将该行存入本会话变量:
read rs03_before_condition
检查选中的文件。如果路径或阶段错误,重新选择并检查;没有备份时,参考实现修复,不要猜后缀。
cat "$rs03_before_condition/condition.rs"
确认它是你打算使用的编辑前源码后,此命令会有意只替换允许修改的状态文件:
cp -p "$rs03_before_condition/condition.rs" src/condition.rs
它恢复未完成的解析器,因此要重新实现状态判断,并重复两个逐行命令。下面的参考实现供尝试后比较具体方法使用,不是常规编辑时替换整个模块的做法。
用于比较和恢复的完整状态参考实现
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Condition { Good, Damaged }
impl Condition {
pub fn parse(value: &str) -> Result<Self, String> {
match value {
"good" => Ok(Self::Good),
"damaged" => Ok(Self::Damaged),
_ => Err(format!("condition must be good or damaged, got {value:?}")),
}
}
pub fn name(self) -> &'static str {
match self { Self::Good => "good", Self::Damaged => "damaged" }
}
}
按 ID 收集实物资产
本步骤实现身份收集,保留不同实物资产,并让内容相同的重复扫描只计一次。继续在 Terminal 的 /home/labex/project/field-kit 中操作。阅读更大的源文件:
cat data/scans.tsv
编写代码前,预测唯一物品。资产 a 出现两次,类型和状态相同;a、b、c 是三支独立的 light-a 类型手电筒。资产 d 属于 light-b,它是不同类型,但计划中的显示名称相同。e 和 f 属于计划外类型 other;合法的额外装备不能消失。应有六个实物 ID,不是七个,也不是每个类型或名称一个。
阅读已有收集器框架:
cat src/identity.rs
Scan 包含 asset_id、type_id、condition 和用于诊断的 line。提供的 use crate::Scan 从本库导入该结构体。函数借用 &[Scan],因此不得修改调用方记录或取得其所有权。其 Result 返回唯一记录组成的向量,或有用的冲突说明。
HashMap 可以将每个资产 ID 关联到一条记录。对已有键插入会替换旧值,因此应先查找再插入。分别决定如何处理新 ID、完全相同的重复 ID/类型/状态,以及矛盾重复。不要对整个 Scan 使用派生相等比较:相同内容自然会出现在不同行号。比较类型和状态;两个行号仅保留作诊断信息。
编辑前保存本阶段源码:
rs03_before_identity=$(mktemp -d .edit-before-identity.XXXXXX) && cp -p src/identity.rs "$rs03_before_identity/identity.rs"
nano -c src/identity.rs
起始代码未导入 HashMap。保留 use crate::Scan;,在顶部旁边添加标准库导入:
use std::collections::HashMap;
std 是 Rust 标准库;crate 是本项目的库。导入让名称在此模块中可用,不是在每个文件中都可用。也允许等效的有序映射或排序实现,导入其实际所需名称即可。
用 Ctrl+W 搜索 pub fn collect_assets,按 Enter。将 _scans 改为 scans,保留切片类型。实现函数判断;保持其他定义和已完成的状态模块不变。
一种基于映射的方法从可变 HashMap<String, Scan> 开始。对于每个借用的扫描记录,get(&scan.asset_id) 返回包含早先记录的 Some,或 None。if let Some(previous) 只在记录存在时进入。只要类型或状态任一不一致,就返回 Err,指出完整资产 ID、当前行和首次出现行。format! 可将这些字段组合为拥有自身内容的消息。如果内容相同,continue 跳过这次重复的插入。
遇到新 ID 时,插入克隆的 ID 和记录;.clone() 从借用输入创建拥有自身内容的副本。检查全部扫描后,将保存的记录移入新 Vec,以 Ok 返回这个向量。Vec::new() 创建空向量,push 追加元素。for (_, scan) in by_id 这样的循环解构键值对,_ 忽略键。HashMap 遍历顺序未指定;提供的格式化器会按资产 ID 排序显示,因此内部顺序不必固定。
按 Ctrl+O、Enter 保存,再按 Ctrl+X。运行前检查保存后的函数:
cat src/identity.rs
先比较实际原始行:
cargo run --quiet -- rows data/scans.tsv
其中仍有重复的 a。现在运行完成的收集器:
cargo run --quiet -- assets data/scans.tsv
asset_id type_id condition
a light-a good
b light-a good
c light-a good
d light-b damaged
e other good
f other damaged
确认全部六个 ID、类型和状态。收集视图仅移除了完全相同的重复;输入文件未编辑。同类型的不同 ID、损坏和计划外装备都保留。下一步检查矛盾,必须拒绝整个收集结果,不能默默选择首条或末条记录。
编译错误时,在 nano 中修复报告的行;如果问题是缺少导入,也要修复。仅在撤销已保存错误时,下面的条件性恢复流程使用你自己的收集器编辑前文件。在新 Terminal 中重复 PATH 和 cd,再列出本阶段的真实备份名:
ls -d .edit-before-identity.*
运行 read,粘贴一个准确打印的目录名,按 Enter。恢复前确认内容。如果没有匹配备份,参考实现做具体修复,不要猜路径。
read rs03_before_identity
cat "$rs03_before_identity/identity.rs"
确认目标源码后,只恢复允许修改的身份文件:
cp -p "$rs03_before_identity/identity.rs" src/identity.rs
它恢复原始收集器占位实现,包括原始导入。重新添加实现需要的导入,完成函数后再测试。它不会移除成功的状态解析器,也不会改变原始输入。
用于比较和恢复的完整身份参考实现
尝试自行实现后,比较相关判断,通过 nano 局部修复。这是用于准备后续独立单元的唯一完整身份源码。
use crate::Scan;
use std::collections::HashMap;
pub fn collect_assets(scans: &[Scan]) -> Result<Vec<Scan>, String> {
let mut by_id: HashMap<String, Scan> = HashMap::new();
for scan in scans {
if let Some(previous) = by_id.get(&scan.asset_id) {
if previous.type_id != scan.type_id || previous.condition != scan.condition {
return Err(format!(
"line {}: conflicting asset {} (first at line {})",
scan.line, scan.asset_id, previous.line
));
}
continue;
}
by_id.insert(scan.asset_id.clone(), scan.clone());
}
let mut unique = Vec::new();
for (_, scan) in by_id {
unique.push(scan);
}
Ok(unique)
}
拒绝冲突并从输入错误中恢复
本步骤区分矛盾证据、未知状态和错误路径。继续在 Terminal 的项目目录中操作。接下来的失败均为预期:应打印错误并返回非零退出状态。不要删除或修复保留的源测试文件来让错误消失。
下面的错误示例展示参考实现的措辞。你的消息可以使用其他文字:冲突必须指出实际路径、完整资产 ID 和两个源行号;未知状态必须指出实际路径和行号,解释被拒绝的值。目标不是逐字匹配说明文字。
先阅读矛盾证据:
cat data/conflict.tsv
它将资产 a 同时描述为 good 和 damaged。运行收集器:
cargo run --quiet -- assets data/conflict.tsv
data/conflict.tsv: line 3: conflicting asset a (first at line 2)
提供的命令接口会将实际输入路径加到收集器错误前。不打印成功表格。末条记录覆盖前条的实现会隐藏证据,仅反转文件就可能改变结果。内容相同的重复允许存在,但矛盾需要在本练习之外调查。
重新选择合法源文件恢复:
cargo run --quiet -- assets data/scans.tsv
现在故意读取含未知状态的测试文件:
cargo run --quiet -- rows data/bad-status.tsv
data/bad-status.tsv: line 2: condition must be good or damaged, got "broken"
这是格式判断,不是一件损坏资产:damaged 是合法数据,broken 不属于本课程受控词汇。读取合法损坏记录恢复:
cargo run --quiet -- rows data/one-damaged.tsv
最后故意使用不存在的单数文件名:
cargo run --quiet -- assets data/scan.tsv
错误指出 data/scan.tsv,说明找不到文件。这不同于已有文件内部的行错误。修正路径,保持实际源文件不变:
cargo run --quiet -- assets data/scans.tsv
六资产表应重新出现。如果 Rust 编辑有误,用 nano 重新打开指出的源码,修复相关函数。要撤销已保存错误,使用状态或身份阶段的检查点,并按前一步明确的 ls/read/cat 确认流程操作;只恢复该允许修改的源码,重新实现,再运行这个最终命令。未保存的 nano 修改则可用 Ctrl+X 和 N 丢弃。两种情况都不需要再次运行 setup 或编辑受保护输入。程序现在能区分源错误与合法的重复、损坏及额外记录。下一实验将用这些唯一身份计算缺口。
总结
你实现了一个包含两种状态的枚举解析器,并按实物资产 ID 收集装备。不同的身份得以保留,相同的重复扫描没有增加数量,矛盾记录则产生带有来源信息的错误,不会悄悄覆盖证据。你还在保留原文件的同时,从未知状态和错误文件名中恢复。可靠的资产记录是解释各类型缺口与富余数量的基础。



