这篇文章会详细总结 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 () { let a = '\u{D800}'; 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)); }
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 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); }
除了 字节字面量
,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 4 5 6 7 8 9 10 11 fn main () { let bs = b"\xFF\xFF" ; let s = "\xFF\xFF" ; } --> 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 foo, foo "foo" , "foo" foo \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 foo foo "foo" "foo" foo \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::OsString
和 std::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 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