Rust中的utf8
什么是编码?
在提到utf8编码之前,我们需要知道,什么是编码?
我们知道,计算机存储的都是01这种比特信息。 计算机是无法直接保存中文或是英文的。 因此美国人就想了个办法,将一些常用的符号和英文用数字编号, 然后将数字转化为二进制表示,于是最早的ascii编码诞生了。
ASCII 编码使用 7 位(0-127)来表示 128 个字符,包括控制字符、数字、字母和标点符号。
但随着计算机的普及,ASCII 已经不够用了。 各个国家开始制定自己的编码标准(如中国的 GB2312、日本的 Shift-JIS),这导致了编码混乱的问题。 为了统一全世界的字符编码,Unicode 应运而生。
什么是utf8编码?
Unicode 为世界上几乎所有的字符都分配了一个唯一的码点(Code Point),例如:
- 'A' 的码点是 U+0041
- '中' 的码点是 U+4E2D
但 Unicode 只是定义了字符和码点的映射关系,并没有规定如何在计算机中存储这些码点。UTF-8 就是 Unicode 的一种实现方式。
UTF-8 是一种变长编码,使用 1 到 4 个字节来表示一个字符:
字节数 | 码点范围 | UTF-8 编码格式 |
---|---|---|
1 | U+0000 - U+007F | 0xxxxxxx |
2 | U+0080 - U+07FF | 110xxxxx 10xxxxxx |
3 | U+0800 - U+FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
4 | U+10000 - U+10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
UTF-8 的优点:
- 向后兼容 ASCII:ASCII 字符用 1 字节表示,编码完全相同
- 自同步性:可以从字节流中任意位置找到字符边界
- 无字节序问题:不像 UTF-16 需要考虑大小端
为什么rust选择了utf8编码?
很多人可能学过c语言,在c语言中,字符串只是一个以 \0
结尾的字节数组,C 语言本身并不关心字符编码:
// C 语言中的字符串
char str[] = "hello";
char str2[] = "你好"; // 编码取决于编译器和系统设置
Rust 选择 UTF-8 作为字符串的默认编码有以下原因:
- 内存效率:对于主要包含 ASCII 的文本(如代码、英文文档),UTF-8 比 UTF-16/UTF-32 更省空间
- 互联网标准:Web 标准主要使用 UTF-8,80% 以上的网页使用 UTF-8
- 安全性:Rust 的
String
和&str
保证是有效的 UTF-8,这在类型层面防止了编码错误 - 零成本抽象:UTF-8 验证的成本可以在字符串创建时一次性完成
在 Rust 中:
let s = String::from("hello"); // 保证是有效的 UTF-8
let s = "你好"; // &str 类型,也保证是有效的 UTF-8
// 从字节创建字符串会进行 UTF-8 验证
let bytes = vec![228, 189, 160];
let s = String::from_utf8(bytes).unwrap(); // "你"
rust中对String/str类型操作的注意事项
在很多语言中,如果想取字符串的第一个字符,通常可以这样写:
# Python
s = "hello"
print(s[0]) # 'h'
// JavaScript
let s = "hello";
console.log(s[0]); // 'h'
但在 Rust 中,不允许用索引直接访问字符串:
let s = "hello";
// let c = s[0]; // 编译错误!
为什么?因为 UTF-8 是变长编码,索引 s[0]
应该返回什么?
- 返回第一个字节?那
s[0]
对于 "你好" 就不是一个完整的字符 - 返回第一个字符?那这个操作就是 O(n) 而不是 O(1)
Rust 选择了不提供索引操作,而是提供了明确的方法:
let s = "你好";
// 字节数 vs 字符数
println!("{}", s.len()); // 6 (字节数)
println!("{}", s.chars().count()); // 2 (字符数)
// 遍历字节
for b in s.bytes() {
println!("{}", b); // 228, 189, 160, 229, 165, 189
}
// 遍历字符
for c in s.chars() {
println!("{}", c); // 你, 好
}
// 获取第 n 个字符(O(n) 操作)
let first = s.chars().nth(0); // Some('你')
字符串切片也必须在字符边界上,否则会 panic:
let s = "你好世界";
// 正确:每个汉字 3 字节
let slice = &s[0..3]; // "你"
let slice = &s[0..6]; // "你好"
// 错误:切在字符中间,会 panic!
// let slice = &s[0..2];
特例独行的Python
有趣的是,Python 3 采用了一种独特的字符串实现方式。
Python 字符串在内部使用灵活的字符串表示(Flexible String Representation):
# Python 会根据字符串内容选择最优编码
s1 = "hello" # 内部使用 Latin-1 (1 字节/字符)
s2 = "你好" # 内部使用 UCS-2 (2 字节/字符)
s3 = "hello🦀" # 内部使用 UCS-4 (4 字节/字符)
# 因此索引操作是 O(1)
print(s2[0]) # "你"
这种做法的优缺点:
优点:
- 索引和切片是 O(1) 操作
- 不用担心字符边界问题
缺点:
- 内存开销大:即使只有一个 emoji,整个字符串都要用 4 字节/字符
- 字符串拼接可能需要重新编码
- 与外部系统交互时需要编解码(如读写文件、网络传输通常是 UTF-8)
Rust 选择了 UTF-8,这意味着:
- 更高的内存效率
- 与外部系统交互无需编解码
- 代价是索引操作需要更谨慎
两种设计各有取舍,Rust 的选择更符合系统编程的需求。