0%

深入理解 Rust(1):Rust 中的字符串

这篇文章会详细总结 Rust 字符串相关的知识,包括 String&str 的区别、各种字符串字面量的区别、以及 OsString/&OsStr、CString/&CStr 等类型的作用。

Unicode 字符集及相关编码方式

在深入 Rust 字符串之前,我们首先需要理解 Unicode 字符集及相关编码方式。广义的 Unicode 标准定义了一个 Unicode 字符集和一系列编码规则(例如 UTF-8、UTF-16、UTF-32)。

Unicode 字符集为每个字符都分配了一个码位(Code Point):

  • Unicode 字符集的编码空间为 0 ~ 0x10FFFF,Unicode 码位就是从该编码空间中为字符分配对应的值
  • 在 Unicode 标准中,码位使用 16 进制编写,并加上前缀 U+,例如 U+0041

Unicode 标量值(Unicode scalar value) 是 Unicode 编码空间中除去代理码位(Surrogate code point)所剩余的所有码位。代理码位为 U+D800~U+DFFF,它们保留给 UTF-16 编码规则使用,所以代理码位不允许分配给抽象字符。因此 Unicode 编码空间中,只有 Unicode 标量值 可以通过编码规则(例如 UTF-8、UTF-16、UTF-32)来生成编码单元序列

这里重点说明一下 UTF-8 编码规则,UTF-8 使用 8bit 编码单元,将每个 Unicode 标量值 映射成 1~4 个 8bit 编码单元,它是一种变长编码方案。

Rust 中的字符

Rust 使用 char 原始类型来表示单个 字符,更具体来说,这里的字符指的是一个 Unicode 标量值(Unicode scalar value)。根据上文介绍的 Unicode 知识,合法的 Unicode 标量值 区间为 0x0 ~ 0xD7FF`` 和 0xE000 ~ 0x10FFFF(即 0 ~ 0x10FFFF-0xD800~0xDFFF`)。

字符字面量(Character literals)的完整定义如下,其使用 '' 包含字符内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
Lexer
CHAR_LITERAL :
' ( ~[' \ \n \r \t] | QUOTE_ESCAPE | ASCII_ESCAPE | UNICODE_ESCAPE ) ' SUFFIX?

QUOTE_ESCAPE :
\' | \"

ASCII_ESCAPE :
\x OCT_DIGIT HEX_DIGIT
| \n | \r | \t | \\ | \0

UNICODE_ESCAPE :
\u{ ( HEX_DIGIT _* )1..6 }

以下都是合法的表示字符 A 的方法:

1
2
3
4
5
6
7
8
9
fn main() {
let a1 = 'A';
let a2 = '\x41';
let a3 = '\u{41}';

println!{"{}", a1};
println!{"{}", a2};
println!{"{}", a3};
}

而以下尝试使用非 Unicode 标量值定义一个 char,则是非法的:

1
2
3
4
5
6
7
fn main() {
// compiler error
let a = '\u{D800}';

// Panics; from_u32 returns None.
let a = char::from_u32(0xD800) .unwrap();
}
1
2
3
4
2 |     let a = '\u{D800}';
| ^^^^^^^^ invalid escape
|
= help: unicode escape must not be a surrogate

由于 Rust 中的 char 类型保存的是 Unicode 标量值,所以它占据 4 个字节,可以表示比 ASCII 多得多的字符内容:

1
2
3
4
5
6
7
8
9
10
11
use std;

fn main() {
let a = 'a';
let z = '中';

println!("{}", a);
println!("{}", z);
println!("{}", std::mem::size_of_val(&a));
println!("{}", std::mem::size_of_val(&z));
}
1
2
3
4
5
# cargo run .
a

4
4

str 类型

str 类型也被称为字符串切片(string slice),由于 str 类型是 DST 类型(Dynamic Sized Type,动态大小类型,即该类型的大小是不固定的),所以通常使用它的借用形式 &str。字符串字面量的类型就是 &'static str

如下定义了字符串字面量,两种形式完全等价:由于字符串字面量具有 static 生命周期,所以它在整个程序生命周期内都是有效的。

1
2
let hello_world = "Hello, World!";
let hello_world: &'static str = "Hello, World!";

&str 由两部分构成:指向字节序列的指针和一个 length 字段。需要注意,字符串切片所指向的字节序列总是有效的 UTF-8 编码字节序列

  • as_ptr() 方法会返回指向 u8 的指针,该指针就是指向字符串切片字节序列的第一个字节
  • len() 方法返回字符串切片的长度,注意这里的长度是字节长度,而不是 char 的个数,所以这个返回值可能和人类所感觉的字符串长度不一致(因为在 UTF-8 编码下一个字符可能需要多个字节)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn print_type_of<T>(_: &T) {
println!("{}", std::any::type_name::<T>())
}

fn main() {
let s = "Hello";
let z = "中文";

let ps = s.as_ptr();
let pz = z.as_ptr();


print_type_of(&ps);
println!("{}, {}", s.len(), s.chars().count());

print_type_of(&pz);
println!("{}, {}", z.len(), z.chars().count());
}

```Rust
*const u8
5, 5
*const u8
6, 2

可以通过 as_bytes() 方法将字符串切片转换为字节切片(&[u8])。但是并不是所有的字节切片(&[u8)都可以转换为字符串切片,因为这些字节序列可能不是有效的 UTF-8 编码序列。from_utf8() 函数可以检查 字节切片 是否能够转换为 字符串切片。下面的例子中,第二个转换就会失败,因为它故意少提供了一个字节,不再是有效的 UTF-8 编码字节序列:

1
2
3
4
5
6
fn main() {
let s = "中文";

println!("{}", std::str::from_utf8(&s.as_bytes()[..]).unwrap());
println!("{}", std::str::from_utf8(&s.as_bytes()[0..s.len() - 1]).unwrap());
}
1
2
3
4
5
6
7
# cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/str_case2 .`
中文
thread 'main' panicked at src/main.rs:5:71:
called `Result::unwrap()` on an `Err` value: Utf8Error { valid_up_to: 3, error_len: None }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

字符串字面量的完整定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
Lexer
STRING_LITERAL :
" (
~[" \ IsolatedCR]
| QUOTE_ESCAPE
| ASCII_ESCAPE
| UNICODE_ESCAPE
| STRING_CONTINUE
)* " SUFFIX?

STRING_CONTINUE :
\ followed by \n

所以字符串字面量是以 "" 包含的 Unicode 标量值序列,它以 UTF-8 格式进行编码,以下都是表示 hello 字符串的方式:

1
2
3
4
5
6
7
8
9
fn main() {
let s1 = "hello";
let s2 = "\x68\x65\x6c\x6c\x6f";
let s3 = "\u{68}\u{65}\u{6c}\u{6c}\u{6f}";

println!("{}", s1);
println!("{}", s2);
println!("{}", s3);
}

String

String 是UTF-8 编码的、可修改的字符串,字符串中保存的内容也必须是合法的 Unicode 标量值序列。String 被存储为由字节组成的 vector(Vector),但是它的字节序列一定是合法的 UTF-8 编码序列。String 所保存的字符串内容分配在堆中,是可增长的并且不以 NULL 结尾。

&str 是一个字符串切片引用,可以理解为它指向了某段字符串内容,这些字符串内容可以存储在程序本身的二进制文件中(例如字符串字面量)、堆中(例如 String)或栈中,如下示例演示了这一点:

1
2
3
4
5
6
7
8
9
fn main() {
let s1 = "hello";
let s2 = &String::from("hello")[..];

let bytes: [u8;5] = [104, 101, 108, 108, 111];
let s3 = std::str::from_utf8(&bytes[..]).unwrap();

println!("{} {} {}", s1, s2, s3);
}

String 类型由三部分组成:指针、长度和容量,相比于 &str 增加了一个容量字段。String 对其所指向的字符串内容拥有所有权,而 &str 则只是某段字符串内容的引用。

接下来再来看看 String 类型和 &str 之间的相互转换,要将 &str 转换为 String 比较简单,以下两种方式都是可行的:

1
2
let s1 = String::from("hello");
let s2 = "hello".to_string();

而要将 String 转换为 &str 则更为简单,取 String 的引用即可。因为 String 实现了 Deref trait,所以 Rust 的隐式解引用转换规则可以将 &String 转换为 &str 以得到指向整个 String 字符串内容的 字符串切片。所以需要从 String 得到 &str 时,以下两种方式都是可以的

1
2
3
4
5
6
7
8
fn main() {
let s = String::from("hello");

let s1 = &s;
let s2 = &s[..];

println!("{} {}", s1, s2);
}

字节字面量字节字符串字面量

Rust 还支持定义 字节字面量,它的形式化定义如下:

1
2
3
4
5
6
7
8
9
BYTE_LITERAL :
b' ( ASCII_FOR_CHAR | BYTE_ESCAPE ) ' SUFFIX?

ASCII_FOR_CHAR :
any ASCII (i.e. 0x00 to 0x7F), except ', \, \n, \r or \t

BYTE_ESCAPE :
\x HEX_DIGIT HEX_DIGIT
| \n | \r | \t | \\ | \0 | \' | \"

字节字面量b 作为前缀,之后是用单引号包含的 ASCII 字符或者字节转义符。它和 字符字面量 最本质的区别是:字符字面量 包含的是 Unicode 标量值,它的类型是 char,占据 4 个字节;而 字节字面量 表示一个字节值,它的类型其实是 u8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn print_type_of<T>(_: &T) {
println!("{}", std::any::type_name::<T>())
}

fn main() {
let b = b'a';
let c = 'a';
let be = b'\xFF';

print_type_of(&b);
println!("{}", std::mem::size_of_val(&b));

print_type_of(&c);
println!("{}", std::mem::size_of_val(&c));
println!("{}", be);
}
1
2
3
4
5
6
# cargo run .
u8
1
char
4
255

除了 字节字面量,Rust 还提供了 字节字符串常量,其定义如下:

1
2
3
4
5
BYTE_STRING_LITERAL :
b" ( ASCII_FOR_STRING | BYTE_ESCAPE | STRING_CONTINUE )* " SUFFIX?

ASCII_FOR_STRING :
any ASCII (i.e 0x00 to 0x7F), except ", \ and IsolatedCR

一个长度为 n 字节的 字节字符串常量,其类型为 &'static [u8; n]。虽然 字节字符串 和普通的字符串(String、&str)所指向的内容都是字节序列,但是有一个显著区别:普通字符串的字节序列一定是合法的 UTF-8 编码序列,而 字节字符串 则没有这个要求,它可以是任意的字节序列

1
2
3
4
5
6
7
8
9
10
11
fn print_type_of<T>(_: &T) {
println!("{}", std::any::type_name::<T>())
}

fn main() {
let bs = b"hello";
let s = "hello";

print_type_of(&bs);
print_type_of(&s);
}
1
2
3
# cargo run .
&[u8; 5]
&str
1
2
3
4
5
6
7
8
9
10
11
fn main() {
let bs = b"\xFF\xFF";
let s = "\xFF\xFF";
}

# cargo run .
--> src/main.rs:7:14
|
7 | let s = "\xFF\xFF";
| ^^^^ must be a character in the range [\x00-\x7f]

原始字符串字面量

Rust 还支持原始字符串字面量(Raw string literals),对于原始字符串字面量,Rust 不会处理任何转义字符。原始字符串以 r 开头,之后跟随 n 个 # 字符(0 <= 0 < 256)以及一个 " 字符,接下来就是原始字符串的内容,原始字符串中可以包含任意的 Unicode 标量值,之后以一个 " 字符以及 n 个 # 作为结尾。

原始字符串的字面量的形式化定义如下:

1
2
3
4
5
6
7
Lexer
RAW_STRING_LITERAL :
r RAW_STRING_CONTENT SUFFIX?

RAW_STRING_CONTENT :
" ( ~ IsolatedCR )* (non-greedy) "
| # RAW_STRING_CONTENT #

如下是 Rust 原始字符串的一些示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn main() {
let s1 = "foo";
let rs1 = r"foo";

let s2 = "\"foo\"";
let rs2 = r#""foo""#;

let s3 = "foo #\"# bar";
let rs3 = r##"foo #"# bar"##;

let s4 = "\\x52";
let rs4 = r"\x52";

let s5 = "\x52";
let ss5 = "R";
let rs5 = r"R";

println!("{}, {}", s1, rs1);
println!("{}, {}", s2, rs2);
println!("{}, {}", s3, rs3);
println!("{}, {}", s4, rs4);
println!("{}, {}, {}", s5, ss5, rs5);
}
1
2
3
4
5
6
# cargo run .
foo, foo
"foo", "foo"
foo #"# bar, foo #"# bar
\x52, \x52
R, R, R

原始字节字符串字面量

字节字符串字面量也有对应的 raw 形式,称为 原始字节字符串字面量(Raw byte string literals),它和 原始字符串字面量 的形式类似,但是是以 br 开头,而且 原始字节字符串字面量 的内容只能包含 ASCII 字符,同样不会处理任何转义字符。

原始字符串字面量 的形式化定义如下:

1
2
3
4
5
6
7
8
9
10
Lexer
RAW_BYTE_STRING_LITERAL :
br RAW_BYTE_STRING_CONTENT SUFFIX?

RAW_BYTE_STRING_CONTENT :
" ASCII* (non-greedy) "
| # RAW_BYTE_STRING_CONTENT #

ASCII :
any ASCII (i.e. 0x00 to 0x7F)

如下是使用 原始字节字符串字面量 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn main() {
let bs1: &[u8] = b"foo";
let brs1: &[u8] = br"foo";

let bs2: &[u8] = b"\"foo\"";
let brs2: &[u8] = br#""foo""#;

let bs3: &[u8] = b"foo #\"# bar";
let brs3: &[u8] = br##"foo #"# bar"##;

let bs4: &[u8] = b"\\x52";
let brs4: &[u8] = br"\x52";

let bs5: &[u8] = b"\x52";
let bss5: &[u8] = b"R";
let brs5: &[u8] = br"R";

println!("{} {}", std::str::from_utf8(&bs1).unwrap(), std::str::from_utf8(&brs1).unwrap());
println!("{} {}", std::str::from_utf8(&bs2).unwrap(), std::str::from_utf8(&brs2).unwrap());
println!("{} {}", std::str::from_utf8(&bs3).unwrap(), std::str::from_utf8(&brs3).unwrap());
println!("{} {}", std::str::from_utf8(&bs4).unwrap(), std::str::from_utf8(&brs4).unwrap());
println!("{} {} {}", std::str::from_utf8(&bs5).unwrap(), std::str::from_utf8(&bss5).unwrap(), std::str::from_utf8(&brs5).unwrap());
}
1
2
3
4
5
6
# cargo run .
foo foo
"foo" "foo"
foo #"# bar foo #"# bar
\x52 \x52
R R R

OsString 和 OsStr

String&str 所指向的字符串一定要是合法的 UTF-8 编码序列,而考虑到以下事实:

  • 在 UNIX 平台上,字符串通常是非 0 字节的任意序列,通常被解释为 UTF-8 序列
  • 在 Windows 平台上,字符串通常是非 0 16-bit 值的任意序列,通常被解释为 UTF-16 序列,
  • 在 Rust 中,字符串总是有效的 UTF-8 序列(可以包含 0 字节)

为了解决 Rust 字符串平台原生字符串 之间的差异,尤其是允许 Rust 字符高效地转换为 平台原生字符串,Rust 提供了 std::ffi::OsStringstd::ffi::OsStr 类型。OsString&OsStr 的关系如同 String&str 的关系,前者对字符串内容拥有所有权,而后者只是字符串内容的引用。

其实在 OsString 内部,字符串仍然是以字节值(8-bit 值)序列的方式存储,但是是采用一种 a less-strict variant of UTF-8 的编码方式(也被称为 WTF-8)。

CString 和 CStr

我们知道,C 风格字符串都是以 \0 字符结尾,而 Rust 标准字符串主要是合法的 UTF-8 字节序列即可,为了方便与 C 语言交互,Rust 提供了 Struct std::ffi::CString 类型,该类型表示与 C 兼容的、以 \0 字符作为结尾的字符串(这也要求字符串中间不能再有 \0 字符了)。

同样,CString&CStr 的关系如同 String&str 的关系,前者对字符串内容拥有所有权,而后者只是字符串内容的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
use std::ffi::CString;

fn main() {
let bytes1: &[u8] = &[65, 66];
let s1 = CString::new(bytes1).expect("new failed");
println!("{}", s1.as_bytes().len());
println!("{}", s1.as_bytes_with_nul().len());
println!("{}", s1.into_string().expect("into string failed"));

let bytes2: &[u8] = &[65, 66, 0, 65];
let s2 = CString::new(bytes2).expect("new failed");
println!("{}", s2.into_string().expect("into string failed"))
}
1
2
3
4
5
6
7
# cargo run .
2
3
AB
thread 'main' panicked at src/main.rs:11:35:
new failed: NulError(2, [65, 66, 0, 65])
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Reference