8.4.0. 本章内容
第八章主要讲的是Rust中常见的集合。Rust中提供了很多集合类型的数据结构,这些集合可以包含很多值。但是第八章所讲的集合与数组和元组有所不同。
第八章中的集合是存储在堆内存上而非栈内存上的,这也意味着这些集合的数据大小无需在编译时就确定,在运行时它们可以动态地变大或变小。
本章主要会讲三种集合:Vector、String(本文) 和HashMap
喜欢的话别忘了点赞、收藏加关注哦,对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
8.4.1. 不能使用索引来访问String
Rust中的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 `SliceIndex<str>` is not implemented for `{integer}`, which is required by `String: Index<_>`
= 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>
= help: the trait `SliceIndex<[_]>` is implemented for `usize`
= help: for that trait implementation, expected `[_]`, found `str`
= note: required for `String` to implement `Index<{integer}>`
报的错是类型String
无法使用整数来进行索引,继续往下看到=help
这一行,这里提示了String
这个类型没有实现index<{integer}>
(index是索引的意思,integer是整数的意思)这个trait。
8.4.2. String类型的内部表示
String类型是对Vec<u8>
的包装,u8
也就是byte字节。我们可以通过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标量值,而西里尔字母每个Unicode标量值都对应两个字节。
通过这个例子你可以发现,String
的数字索引并不能总是对应道一个完整的Unicode标量值,因为有的Unicode标量值会占不止一个字节,而数字索引注定只能读取到一个字节的值。
再举个例子,西里尔语里的З
(不是数字)这个字母对应的是两个字节,而这两个字节的值分别是是208和151。假如说数字索引是允许的,那么我取Здравствуйте
的索引0的值就会是208,而208本身又是无意义的字符(因为缺少第二个字节组不成一个Unnicode标量值)。所以为了避免这种无法立即发现的bug,Rust封杀了数字索引String
,也就是在开发的早期阶段杜绝可能的误解。
8.4.3. 字节、标量值、字形簇
Rust中有三种看待字符串的方式:字节(Bytes)、标量值(Scalar Values)和字形簇(Grapheme Clusters)。其中字形簇是最接近我们说说的字母的概念的。
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()
方法能够获得这段字符串所对应的标量值,输出如下:
न म स ् त े
它有4个实际的字母,而第四个和第六个标量值代表的是音标,单独存在没有任何意义,得于前面的东西放在一起算是一个字母。
这里也解释了为什么这个梵文实际上有18个字节,因为一个梵文占3个字节,这段字符串加上隐藏着的音标一共6个字符,把这两个数字相乘可以得到18这个数字,也就是18个字节。
3. 字形簇
因为从String
里获得字形簇很复杂,所以Rust标准库没有提供这个功能,这里也就不做演示,但是可以去crate.io找第三方的库来实现这个功能。
总之,这串梵文如果以字形簇的格式打印出来会是:
这个样子。
8.4.4. 不能使用索引来访问String的原因
- 数字索引取出来值的可能并不完整,无法组成一个Unicode标量值,导致无法第一时间察觉的错误
- 索引操作会消耗一个常量时间,也就是O(1),而
String
无法保证这个时间,因为它需要从头到尾遍历所有内容从而确定有多少个合法的字符。
8.4.5. 切割String
可以使用[]
,在里面填上范围来创建字符串切片(关于字符串切片的详细内容在4.5. 切片(Slice),这里不再赘述)。如下例:
fn main() {
let hello = String::from("Здравствуйте");
let s = &hello[0..4];
println!("{}", s);
}
刚才也说了一个西里尔字母占两个字节,这里的字符串切片切的是字符串的前4个字节,也就是前两个字母,看一下输出:
Зд
那如果字符串切片切的是2前三个字节呢?也就意味着切片的内容会是第一个字母加上半个第二个字母,这种情况会怎么样呢?看下面的例子:
fn main() {
let hello = String::from("Здравствуйте");
let s = &hello[0..3];
println!("{}", s);
}
输出:
byte index 3 is not a char boundary; it is inside 'д' (bytes 2..4) of `Здравствуйте`
程序触发了panic!
,错误信息是:索引3不是一个char边界。也就是说在切割的时候必须沿着char的边界来切割,对于这个西里尔语言来说就是2个2个字节地切割。
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);
}
}
- 对于字形簇,标准库未提供方法,但是可以找第三方库。