MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Rust 字符类型的编码处理

2021-12-175.8k 阅读

Rust 字符类型基础

在 Rust 中,char 类型代表了一个 Unicode 标量值,它占用 4 个字节。Unicode 是一个庞大的字符集,包含了世界上几乎所有语言的字符、符号等。每个 Unicode 标量值都有一个唯一的编号,范围从 U+0000U+D7FFU+E000U+10FFFF

字符字面量

在 Rust 中定义一个字符字面量非常简单,使用单引号括起来:

let c: char = 'a';
let chinese_char: char = '中';
let symbol_char: char = '☢';

这里,'a' 是一个 ASCII 字符,它在 Unicode 中的标量值为 U+0061'中' 是一个中文字符,其 Unicode 标量值为 U+4E2D'☢' 是一个辐射符号,Unicode 标量值为 U+2622

字符类型与编码的关系

虽然 char 类型占用 4 个字节,但它并不直接对应任何特定的文本编码,如 UTF - 8、UTF - 16 或 UTF - 32。Rust 中的字符串(String&str)通常以 UTF - 8 编码存储,而 char 类型是用于处理 Unicode 标量值的抽象。

例如,一个简单的字符串 "hello",在内存中以 UTF - 8 编码存储,每个字符(在这种情况下,都是 ASCII 字符,UTF - 8 编码与 ASCII 编码相同)占用 1 个字节。但是,如果字符串包含非 ASCII 字符,情况就会变得复杂。比如字符串 "你好",在 UTF - 8 编码下,'你' 占用 3 个字节,'好' 也占用 3 个字节,总共 6 个字节。然而,当我们从这个字符串中提取 char 类型的值时,Rust 会将每个 Unicode 标量值作为一个 char 来处理。

UTF - 8 编码基础

为了更好地理解 Rust 中字符类型的编码处理,先来深入了解一下 UTF - 8 编码。UTF - 8 是一种变长字符编码,它可以使用 1 到 4 个字节来表示一个 Unicode 标量值。

UTF - 8 编码规则

  1. 单字节字符(ASCII 兼容):对于 Unicode 标量值范围在 U+0000U+007F 的字符,UTF - 8 编码与 ASCII 编码相同,使用 1 个字节表示。例如,字符 'a'U+0061)的 UTF - 8 编码为 01100001
  2. 双字节字符:对于 Unicode 标量值范围在 U+0080U+07FF 的字符,使用 2 个字节表示。第一个字节的格式为 110xxxxx,第二个字节的格式为 10xxxxxx。例如,字符 'é'U+00E9,十六进制为 0xE9),其 UTF - 8 编码为 11000110 10101001
  3. 三字节字符:对于 Unicode 标量值范围在 U+0800U+FFFF 的字符,使用 3 个字节表示。第一个字节的格式为 1110xxxx,第二个字节的格式为 10xxxxxx,第三个字节的格式为 10xxxxxx。例如,字符 '中'U+4E2D),其 UTF - 8 编码为 11100100 10111000 10101101
  4. 四字节字符:对于 Unicode 标量值范围在 U+10000U+10FFFF 的字符,使用 4 个字节表示。第一个字节的格式为 11110xxx,第二个字节的格式为 10xxxxxx,第三个字节的格式为 10xxxxxx,第四个字节的格式为 10xxxxxx。例如,字符 '𐍈'U+10348),其 UTF - 8 编码为 11110001 10000011 10010000 10101000

Rust 中字符串与 UTF - 8 的关系

Rust 中的字符串类型 String&str 内部都是以 UTF - 8 编码存储的。这意味着当我们创建一个字符串时,Rust 会自动将我们提供的字符序列转换为 UTF - 8 编码。

let s1 = String::from("hello");
let s2 = "你好";

s1 中,每个字符都是 ASCII 字符,所以其 UTF - 8 编码与 ASCII 编码相同。而在 s2 中,'你''好' 都是中文字符,分别占用 3 个字节的 UTF - 8 编码。

Rust 中字符类型的编码转换

char 转换为 UTF - 8 字节序列

在 Rust 中,可以通过 char 类型的 encode_utf8 方法将 char 转换为 UTF - 8 字节序列。该方法接受一个 String&mut String 作为参数,并将 char 的 UTF - 8 编码追加到这个字符串中。

let c: char = '中';
let mut s = String::new();
c.encode_utf8(&mut s);
println!("{}", s);

// 或者直接获取字节数组
let bytes: Vec<u8> = c.encode_utf8(Vec::new()).into();
println!("{:?}", bytes);

在上述代码中,首先定义了字符 '中',然后使用 encode_utf8 方法将其转换为 UTF - 8 编码并追加到 String 类型的 s 中。另外,也可以通过 encode_utf8 返回的 Cow<str> 类型,调用 into 方法将其转换为 Vec<u8> 类型的字节数组。

从 UTF - 8 字节序列转换为 char

Rust 中的 char 类型提供了 from_utf8 方法来从 UTF - 8 字节序列创建 char。但是,需要注意的是,这个方法是不安全的,因为它假设提供的字节序列是一个有效的 UTF - 8 编码。

let bytes: &[u8] = &[228, 184, 173]; // '中' 的 UTF - 8 编码
unsafe {
    let c = char::from_utf8_unchecked(bytes);
    println!("{}", c);
}

在上述代码中,通过 char::from_utf8_unchecked 方法从给定的字节序列创建了 char 类型的 c。由于这是一个不安全的操作,所以需要在 unsafe 块中执行。如果字节序列不是有效的 UTF - 8 编码,可能会导致未定义行为。

如果想要安全地从 UTF - 8 字节序列创建 char,可以使用 char::from_utf8 方法,它会返回一个 Result<char, FromUtf8Error>

let bytes: &[u8] = &[228, 184, 173];
let result = char::from_utf8(bytes);
match result {
    Ok(c) => println!("{}", c),
    Err(e) => println!("Error: {:?}", e),
}

在上述代码中,char::from_utf8 方法会检查字节序列是否为有效的 UTF - 8 编码。如果是,则返回 Ok(char),否则返回 Err(FromUtf8Error),通过 match 语句可以对结果进行相应的处理。

字符串与字符类型的编码处理

遍历字符串中的字符

在 Rust 中遍历字符串中的字符时,实际上是在遍历 Unicode 标量值。由于字符串是以 UTF - 8 编码存储的,Rust 会自动处理 UTF - 8 编码到 char 的转换。

let s = "你好,世界";
for c in s.chars() {
    println!("{}", c);
}

在上述代码中,通过 s.chars() 方法遍历字符串 s 中的每个字符。Rust 会根据 UTF - 8 编码将字符串解析为一个个 char 类型的值。

在字符串中查找字符

可以使用 find 方法在字符串中查找某个字符。该方法会返回字符第一次出现的索引位置,如果没有找到则返回 None

let s = "你好,世界";
let index = s.find('世');
match index {
    Some(i) => println!("字符 '世' 出现在索引位置: {}", i),
    None => println!("字符 '世' 未找到"),
}

在上述代码中,s.find('世') 会在字符串 s 中查找字符 '世'。由于字符串是以 UTF - 8 编码存储的,find 方法会正确处理多字节字符的查找。

字符串的切片与字符编码

当对字符串进行切片操作时,需要注意切片的位置必须是 UTF - 8 编码的边界。否则,会导致未定义行为。

let s = "你好,世界";
let slice = &s[0..3]; // 正确,'你' 的 UTF - 8 编码长度为 3 字节
println!("{}", slice);

// 下面的操作是错误的,会导致未定义行为
// let wrong_slice = &s[1..4];

在上述代码中,&s[0..3] 是一个正确的切片操作,因为它从 UTF - 8 编码的起始位置开始,并且切片长度正好是一个字符('你')的 UTF - 8 编码长度。而 &s[1..4] 是错误的,因为它没有从 UTF - 8 编码的边界开始切片。

高级字符编码处理

处理代理对(Surrogate Pairs)

在 Unicode 中,对于 U+10000U+10FFFF 范围的字符,在 UTF - 16 编码中会使用代理对来表示。虽然 Rust 的字符串是以 UTF - 8 编码存储的,但在处理一些跨编码转换或者与其他系统交互时,可能会遇到代理对的概念。

在 Rust 中,char 类型直接表示 Unicode 标量值,不涉及代理对的处理。但是,当需要将字符串转换为 UTF - 16 编码时,就需要处理代理对。

use std::convert::TryFrom;
use std::char::decode_utf16;

let c: char = '𐍈'; // U+10348
let utf16: Vec<u16> = char::encode_utf16(&[c]).collect();
println!("{:?}", utf16);

let decoded: Result<Vec<char>, _> = decode_utf16(utf16.iter().cloned())
   .collect();
match decoded {
    Ok(chars) => println!("{:?}", chars),
    Err(e) => println!("解码错误: {:?}", e),
}

在上述代码中,首先将字符 '𐍈' 编码为 UTF - 16 代理对(存储在 utf16 向量中),然后通过 decode_utf16 方法将 UTF - 16 代理对解码回 char 类型的向量。

与其他编码的转换

虽然 Rust 中的字符串默认使用 UTF - 8 编码,但在实际应用中,可能需要与其他编码(如 ASCII、UTF - 16、GB2312 等)进行转换。

对于 ASCII 编码,由于它是 UTF - 8 的子集,转换相对简单。可以使用 std::str::from_utf8String::from_utf8 方法进行转换。

let ascii_bytes: &[u8] = b"hello";
let ascii_str = std::str::from_utf8(ascii_bytes).unwrap();
let ascii_string = String::from(ascii_str);
println!("{}", ascii_string);

在上述代码中,首先将字节数组 b"hello"(这是一个 ASCII 编码的字节数组)通过 from_utf8 方法转换为字符串切片 ascii_str,然后再转换为 String 类型。

对于 UTF - 16 编码,可以使用 encoding_rs 库来进行转换。

use encoding_rs::{UTF_16BE, UTF_8};

let s = "你好";
let utf8_bytes = UTF_8.encode(s).0;
let (utf16_bytes, _, _) = UTF_16BE.encode(&utf8_bytes);
println!("{:?}", utf16_bytes);

在上述代码中,首先将字符串 s 编码为 UTF - 8 字节数组 utf8_bytes,然后使用 UTF_16BE(UTF - 16 大端序)将 UTF - 8 字节数组转换为 UTF - 16 字节数组。

如果需要处理像 GB2312 这样的编码,同样可以使用 encoding_rs 库。

use encoding_rs::GB2312;

let s = "你好";
let utf8_bytes = UTF_8.encode(s).0;
let (gb2312_bytes, _, _) = GB2312.encode(&utf8_bytes);
println!("{:?}", gb2312_bytes);

在上述代码中,将 UTF - 8 编码的字符串转换为 GB2312 编码的字节数组。

字符编码处理中的性能考虑

遍历字符串的性能

在遍历字符串中的字符时,chars() 方法会根据 UTF - 8 编码将字符串解析为 char 类型的值。这个过程在处理大量字符时可能会有一定的性能开销,因为需要逐个字符地解析 UTF - 8 编码。

如果只需要处理字节数据,而不需要将其转换为 char,可以使用 bytes() 方法。例如,在统计字符串中某个字节出现的次数时,使用 bytes() 方法会更高效。

let s = "你好,世界";
let count = s.bytes().filter(|&b| b == b',');
println!("逗号出现的次数: {}", count.count());

在上述代码中,通过 bytes() 方法直接遍历字符串的字节,而不需要进行 UTF - 8 到 char 的解析,提高了性能。

编码转换的性能

编码转换,如从 UTF - 8 转换到其他编码,通常会涉及到内存分配和数据复制。特别是在处理大量数据时,性能开销可能会很明显。

在选择编码转换库时,要考虑其性能。例如,encoding_rs 库在编码转换方面经过了优化,性能相对较好。

另外,尽量减少不必要的编码转换。如果在系统中能够始终保持一种编码(如 UTF - 8),可以避免频繁的编码转换带来的性能损失。

字符串切片的性能

在进行字符串切片操作时,由于需要确保切片位置是 UTF - 8 编码的边界,Rust 会进行一些额外的检查。如果在循环中频繁进行切片操作,可能会影响性能。

一种优化方法是尽量在循环外部进行切片,减少切片操作的次数。

let s = "你好,世界";
let part = &s[0..3]; // 在循环外切片
for _ in 0..1000 {
    // 使用切片后的结果
    println!("{}", part);
}

在上述代码中,在循环外部进行了一次切片操作,然后在循环内部多次使用这个切片结果,避免了在循环中频繁切片带来的性能开销。

字符编码处理中的常见错误与解决方法

无效的 UTF - 8 编码

在处理字符串时,最常见的错误之一是遇到无效的 UTF - 8 编码。这可能是由于数据传输错误、不正确的编码转换等原因导致的。

当使用 from_utf8 方法将字节数组转换为字符串时,如果字节数组不是有效的 UTF - 8 编码,会返回 Err(FromUtf8Error)

let invalid_bytes: &[u8] = &[228, 184, 173, 128]; // 无效的 UTF - 8 编码
let result = String::from_utf8(invalid_bytes.to_vec());
match result {
    Ok(s) => println!("{}", s),
    Err(e) => println!("错误: {:?}", e),
}

在上述代码中,invalid_bytes 包含了无效的 UTF - 8 编码,String::from_utf8 方法会返回 Err,通过 match 语句可以捕获并处理这个错误。

解决方法是在处理字节数据之前,先验证其是否为有效的 UTF - 8 编码。可以使用 std::str::is_utf8 方法来进行验证。

let invalid_bytes: &[u8] = &[228, 184, 173, 128];
if invalid_bytes.is_utf8() {
    let s = String::from_utf8(invalid_bytes.to_vec()).unwrap();
    println!("{}", s);
} else {
    println!("字节数组不是有效的 UTF - 8 编码");
}

在上述代码中,通过 is_utf8 方法先验证字节数组是否为有效的 UTF - 8 编码,然后再进行转换。

切片位置错误

在进行字符串切片时,如果切片位置不是 UTF - 8 编码的边界,会导致未定义行为。

例如:

let s = "你好";
// 下面的操作是错误的,会导致未定义行为
// let slice = &s[1..4];

解决方法是确保切片位置是 UTF - 8 编码的边界。可以先将字符串转换为 char 类型的向量,然后根据字符的位置来确定切片位置。

let s = "你好";
let chars: Vec<char> = s.chars().collect();
let start_index = chars.iter().take(1).map(|c| c.len_utf8()).sum();
let end_index = start_index + chars[1].len_utf8();
let slice = &s[start_index..end_index];
println!("{}", slice);

在上述代码中,先将字符串转换为 char 类型的向量,通过 len_utf8 方法获取每个字符的 UTF - 8 编码长度,从而确定正确的切片位置。

编码转换错误

在进行编码转换时,可能会因为源编码或目标编码的不正确设置而导致错误。

例如,在使用 encoding_rs 库进行编码转换时,如果指定了错误的编码类型:

use encoding_rs::{UTF_16BE, UTF_8};

let s = "你好";
let utf8_bytes = UTF_8.encode(s).0;
// 假设这里错误地使用了 UTF_16LE(小端序)而不是 UTF_16BE
let (wrong_utf16_bytes, _, _) = encoding_rs::UTF_16LE.encode(&utf8_bytes);
println!("{:?}", wrong_utf16_bytes);

在上述代码中,错误地使用了 UTF_16LE 进行编码转换,可能会导致结果不符合预期。

解决方法是仔细检查编码类型的设置,确保使用正确的编码进行转换。在进行复杂的编码转换时,可以先进行一些测试,验证转换结果的正确性。

通过深入了解 Rust 中字符类型的编码处理,包括基础概念、编码转换、性能考虑以及常见错误处理,开发者可以更加高效、准确地处理各种字符编码相关的任务,在开发涉及文本处理的应用程序时能够避免很多潜在的问题。