【Rust自学】8.4. String类型 Pt.2:字节、标量值、字形簇以及字符串的各类操作 8.4 String类型 Pt.2字节、标量值、字形簇以及字符串的各类操作8.4.0. 本章内容第八章主要讲的是 Rust 中常见的集合。Rust 提供了很多集合类型的数据结构这些集合可以包含很多值。但是第八章所讲的集合与数组和元组有所不同。第八章中的集合是存储在堆内存上而非栈内存上的这也意味着这些集合的数据大小无需在编译时就确定在运行时它们可以动态地变大或变小。本章主要会讲三种集合Vector、String本文和 HashMap。8.4.1. 不能使用索引来访问StringRust 中的String不同于其他语言不能用索引访问。如下例fn main() { let s String::from(6657 up up); let a s[0]; }输出error[E0277]: the type str cannot be indexed by {integer} -- src/main.rs:3:15 | 3 | let a s[0]; | ^ string indices are ranges of usize | help: the trait SliceIndexstr is not implemented for {integer} note: you can use .chars().nth() or .bytes().nth() for more information, see chapter 8 in The Book: https://doc.rust-lang.org/book/ch08-02-strings.html#indexing-into-strings note: required for String to implement Index{integer}报错说明String类型无法使用整数来进行索引。继续往下看可以看到String没有实现Index{integer}这个 trait。8.4.2.String类型的内部表示String是对Vecu8的包装其中u8表示字节。我们可以通过String上的len()方法来返回字符串的长度。如下例fn main() { let len String::from(Niko).len(); println!({}, len); }输出4这个字符串采用的是UTF-8编码len的值为4也就是这个字符串占了 4 个字节。所以在这个例子里每个字母占用了一个字节。但情况并不总是这样。比如说我们把字符串换成其他语言这里是西里尔字母写的俄语fn main() { let hello String::from(Здравствуйте); println!({}, hello.len()); }如果你数一下这个字符串会发现有 12 个字母但是输出却是24也就是说在这个语言里面一个字母会占用两个字节中文是一个汉字占三个字节。这里所说的“字母”用专业术语表示就是Unicode 标量值而这里的每个西里尔字母都对应两个字节。通过这个例子你可以发现对String做数字索引并不总能对应到一个完整的 Unicode 标量值因为有的标量值会占不止一个字节而数字索引一次只能读到一个字节。再举个例子西里尔字母З对应两个字节这两个字节的值分别是 208 和 151。假如数字索引是允许的那么取Здравствуйте的索引0就会得到208而单独的208是没有意义的因为它缺少组成一个 Unicode 标量值所需的第二个字节。所以为了避免这种无法立即发现的 bugRust 禁止了对String使用数字索引从而在开发早期就杜绝可能的误解。8.4.3. 字节、标量值、字形簇Rust 中有三种看待字符串的方式字节、标量值和字形簇。其中字形簇最接近我们通常所说的“字母”。1. 字节看个例子fn main() { let s String::from(नमस्ते); // 天城文书写的印地语 for b in s.bytes() { print!({} , b); } }这段天城文字符串看起来好像有 4 个字母。我们使用.bytes()方法来获得它所对应的字节。输出如下224 164 168 224 164 174 224 164 184 224 165 141 224 164 164 224 165 135这 18 个字节就是计算机存储该字符串的样子。2. 标量值我们再来以 Unicode 标量值的形式来看待它fn main() { let s String::from(नमस्ते); for b in s.chars() { print!({} , b); } }使用.chars()方法能够获得这段字符串所对应的标量值。输出如下न म स ् त े它有 6 个标量值其中有些是组合标记而不是独立的字母。它们只有和前面的字符组合在一起才有意义。这也解释了为什么这段天城文字符串占 18 个字节6 个标量值每个占 3 个字节6 × 3 18 个字节。3. 字形簇因为从String里获得字形簇很复杂所以 Rust 标准库没有提供这个功能这里也就不做演示但你可以从 crates.io 找第三方 crate 来实现这个功能。总之如果这串字符串以字形簇的形式打印出来会是这样8.4.4. 为什么String不能被索引数字索引取出来的值可能并不完整无法组成一个完整的 Unicode 标量值从而导致无法第一时间察觉的错误。索引操作理应消耗常量时间也就是O(1)而String无法保证这一点要找到第n个字符必须从字符串开头向后扫描因为每个字符占用的字节数可变。8.4.5. 切割String可以使用[]在里面填上范围来创建字符串切片。关于字符串切片的详细内容见 4.5. 切片Slice。如下例fn main() { let hello String::from(Здравствуйте); let s hello[0..4]; println!({}, s); }刚才也说了一个西里尔字母占两个字节。这里的字符串切片切的是字符串的前 4 个字节也就是前两个字母。输出是Зд那如果字符串切片切的是前三个字节呢也就意味着切片的内容会是第一个字母加上半个第二个字母。这种情况会怎么样呢看下面的例子fn main() { let hello String::from(Здравствуйте); let s hello[0..3]; println!({}, s); }输出thread main panicked at src/main.rs:3:19: end byte index 3 is not a char boundary; it is inside д (bytes 2..4 of string) note: run with RUST_BACKTRACE1 environment variable to display a backtrace程序触发了panic!错误信息是结束字节索引3不是一个char边界。也就是说切割时必须沿着char的边界来切割对于西里尔字母来说就是按两个字节为单位切割。8.4.6. 遍历String对于标量值使用.chars()方法。如下例fn main() { let s String::from(नमस्ते); for b in s.chars() { print!({} , b); } }对于字节使用.bytes()方法。如下例fn main() { let s String::from(नमस्ते); for b in s.bytes() { print!({} , b); } }对于字形簇标准库未提供方法但可以使用第三方 crate。