Rust中的utf8

on 2025-10-02

什么是编码?

在提到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 编码格式
1U+0000 - U+007F0xxxxxxx
2U+0080 - U+07FF110xxxxx 10xxxxxx
3U+0800 - U+FFFF1110xxxx 10xxxxxx 10xxxxxx
4U+10000 - U+10FFFF11110xxx 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 作为字符串的默认编码有以下原因:

  1. 内存效率:对于主要包含 ASCII 的文本(如代码、英文文档),UTF-8 比 UTF-16/UTF-32 更省空间
  2. 互联网标准:Web 标准主要使用 UTF-8,80% 以上的网页使用 UTF-8
  3. 安全性:Rust 的 String&str 保证是有效的 UTF-8,这在类型层面防止了编码错误
  4. 零成本抽象: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 的选择更符合系统编程的需求。

粤ICP备2025368514号-1