Course Builder个人学习工作台 · 本地模式
Rust:从所有权到可测试的命令行工具

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

测试仍应全部通过。无效编码在读取阶段失败,不会生成半份报告。

改用空文件

  1. 将 read_file.rs 第一处路径改成 fixtures/empty.txt。
  2. 运行 cargo run --quiet --example read_file,报告应为四个 0,第二条验证仍成功。
  3. 恢复主示例路径。

独立练习:在 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/ 中四个样本。下一课只替换主入口。

参考资料