14|文件读取:连接外部输入与纯函数
目标:实现返回 io::Result<Report> 的文件分析函数,并验证成功与无效编码两条路径。
统计函数已经独立测试。现在只增加一个边界:把文件读成文本,交给 analyze。读取可能因为路径、权限或内容编码失败,这些不是统计规则的问题,应由文件层返回错误。
Path 表示平台路径,&Path 借用它;路径并不保证是 UTF-8 文本。fs::read_to_string 读取整个文件并验证内容是 UTF-8,返回 io::Result<String>。这里的 io::Result<T> 是 Result<T, io::Error> 的类型别名,仍使用第 8 课的 ? 规则。整个文件会进入内存,因此本课程工具面向能合理装入内存的文本;超大文件需要之后学习流式处理。
只增加读取这一层
保留 src/stats.rs 和测试,完整替换 src/lib.rs:
mod stats;
use std::{fs, io, path::Path};
pub use stats::{Report, analyze};
pub fn analyze_file(path: &Path) -> io::Result<Report> {
let text = fs::read_to_string(path)?;
Ok(analyze(&text))
}
text 在函数结束时被丢弃,但 Report 只含数字,所以可以安全返回。错误在 ? 处原样传播,成功路径没有重复任何计数逻辑。
为避免不同编辑器的换行和编码影响实验,创建 examples/prepare_inputs.rs。它只在你的 TextLab 练习项目中准备固定名称的样本;再次运行会覆盖这四个练习文件:
use std::{fs, io};
fn main() -> io::Result<()> {
fs::create_dir_all("fixtures")?;
fs::write("fixtures/sample.txt", "你好 Rust\n")?;
fs::write("fixtures/empty.txt", "")?;
fs::write("fixtures/crlf.txt", "a\r\nb\r\n")?;
fs::write("fixtures/invalid.bin", [0xff_u8])?;
println!("fixtures ready");
Ok(())
}
create_dir_all 创建目录,已存在时仍可成功;write 创建或覆盖文件。[0xff_u8] 是含一个字节的数组,值为十六进制 FF,不是合法的独立 UTF-8 文本。main 也可以返回 Result,成功用 Ok(()),失败则由运行时报告;下课改成面向用户的错误展示。
再创建 examples/read_file.rs:
use std::{io, path::Path};
use textlab::analyze_file;
fn main() -> io::Result<()> {
let report = analyze_file(Path::new("fixtures/sample.txt"))?;
println!("{}", report.summary());
let error = analyze_file(Path::new("fixtures/invalid.bin"))
.expect_err("invalid UTF-8 must fail");
assert_eq!(error.kind(), io::ErrorKind::InvalidData);
println!("invalid UTF-8 rejected");
Ok(())
}
Path::new 把路径文本借用为路径视图。expect_err 用于这个验证程序:若意外成功就 panic,若失败就取出错误。kind() 提供可比较的错误类别,比依赖操作系统的完整英文错误句子更稳定。
运行前先预测:无效编码会产生一份部分统计报告吗?
cargo run --quiet --example prepare_inputs
cargo run --quiet --example read_file
cargo test --quiet
前两个命令依次产生的预期输出:
fixtures ready
lines=1 words=2 bytes=12 chars=8
invalid UTF-8 rejected
测试仍应全部通过。无效编码在读取阶段失败,不会生成半份报告。
改用空文件
- 将
read_file.rs第一处路径改成fixtures/empty.txt。 - 运行
cargo run --quiet --example read_file,报告应为四个 0,第二条验证仍成功。 - 恢复主示例路径。
独立练习:在 read_file.rs 的 Ok(()) 之前验证 CRLF 文件应有 2 行、2 个词、6 字节、6 个标量值。
可插入的参考检查
这段依赖现有函数作用域中的 imports,插在 Ok(()) 之前:
let crlf = analyze_file(Path::new("fixtures/crlf.txt"))?;
assert_eq!(crlf.lines, 2);
assert_eq!(crlf.words, 2);
assert_eq!(crlf.bytes, 6);
assert_eq!(crlf.chars, 6);
运行同一示例,原输出不变且成功退出就表示断言通过。CRLF 被视为一个行结束符,但其中 CR、LF 仍分别计入字节数和标量值数。
如果报找不到文件,先确认已经运行样本生成器,并从 TextLab 根目录执行命令;相对路径按当前工作目录解析。如果无效编码测试意外成功,检查 invalid.bin 是否被编辑器改写成了字符串 0xff,重新运行生成器。如果 Windows 上显示权限类错误,不要假定一定是编码问题,先检查 kind() 与错误消息。
检查点:保留已有源文件和测试,新增两个 example 与 fixtures/ 中四个样本。下一课只替换主入口。