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

05|字符串与切片:明确文本统计的单位

目标:根据需要选择字节、Unicode 标量值、行或空白分隔词的统计方法。

文本工具最容易出现的错误之一,是计算正确却回答了错误的问题。"你好" 在 UTF-8 中占 6 字节,包含 2 个 Unicode 标量值;而一些视觉上像一个字符的组合,可能包含多个标量值。TextLab 必须明确自己数的是什么。

String 拥有 UTF-8 字节;&str 借用其中一段合法 UTF-8 文本,可来自字面量,也可来自一个 String。切片没有复制对应文本,它携带位置和长度。len() 数字节;chars() 逐个产生 Unicode 标量值;lines() 产生各行;split_whitespace() 按 Unicode 空白切出词片段。这些方法返回迭代器,.count() 消费迭代器并统计元素数;第 9 课再练组合操作。

按空白切词适合简单日志统计,不等于自然语言分词。字符串 "你好世界" 没有空白,会被算作一个词。

用一份输入比较四个单位

创建 examples/strings.rs:

fn main() {
    let text = String::from("你好 Rust\n");
    let view: &str = &text;
    println!("bytes={}", view.len());
    println!("chars={}", view.chars().count());
    println!("words={}", view.split_whitespace().count());
    println!("lines={}", view.lines().count());
    println!("prefix={}", &view[..6]);
}

\n 在字符串里表示一个换行字符。[..6] 按字节取开头到索引 6 之前的范围;这里 6 恰好处在“好”之后的 UTF-8 边界。

运行前先预测:末尾的换行会不会让 lines() 多产生一行?

cargo run --quiet --example strings

预期输出:

bytes=12
chars=8
words=2
lines=1
prefix=你好

两个汉字占 6 字节,空格占 1,Rust 占 4,换行占 1。它们合计 8 个标量值。lines() 不把末尾结束符之后再算成额外空行,但文本中间的空行仍会计入;它识别 LF 和 CRLF 行尾。空串有 0 行。

对照实验:只改变行边界

  1. 把输入改成 "你好 Rust\n\n"。
  2. 再运行,应得到 13 字节、9 个标量值、2 个词、2 行,前缀仍然是“你好”。
  3. 恢复主示例。思考新增换行为什么增加行数,而原来的尾换行没有凭空增加一个空行。

独立练习:用断言验证 "e\u{301}" 的字节数为 3、标量值数为 2;同时验证空串是 0 行,"a\r\nb\r\n" 是 2 行。\u{301} 表示一个组合重音符号,屏幕显示效果可能与单个带重音字母相似。

完整参考实现
fn main() {
    let combined = "e\u{301}";
    assert_eq!(combined.len(), 3);
    assert_eq!(combined.chars().count(), 2);
    assert_eq!("".lines().count(), 0);
    assert_eq!("a\r\nb\r\n".lines().count(), 2);
    println!("text unit checks passed");
}

完整替换实验文件后运行同一命令,预期 text unit checks passed。Unicode 标量值不是字形簇,本课程不会把这个计数叫作“用户可见字符数”。

如果字符串切片在运行时 panic,先检查起止位置是否都在 UTF-8 边界;不能把 [..6] 当成适用于任意文本的“前两个字”。如果试图写 text[0],编译器会拒绝字符串的单整数索引;应先决定要字节、标量值还是字符串片段。一般业务代码应优先使用迭代方法,而不是猜字节边界。

本课新增 examples/strings.rs,并确定最终项目的统计契约:字节、标量值、按空白分词、按 lines() 分行。

参考资料