RUST 0x06 Common Collections
1 Vector
vector,Vec
,能存储相同类型的值。
创建一个Vector
要创建一个空的vector,可以调用Vec::new
函数,如:
let v: Vec = Vec::new();
注意,这里写出类型名Vec
是因为我们没有在这个vector中插入任何值,所以我们就需要让Rust知道我们想要存储什么类型的值。
如果我们在创建vector时就已经插入了值,那么就没有必要再注明类型名了——Rust可以从插入的值推断类型。
如果要创建含初始值的vector,可以使用Rust中的vec!
宏,如:
let v = vec![1, 2, 3];
因为我们已经给了i32
的初始值,所以Rust可以推断出来v
的类型是Vec
。
更新一个Vector
如果想在vector后面添加元素,我们可以用push
method,如:
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
就像对所有变量一样,如果我们想要改变vector的值,我们需要用mut
来让其可变。
此外,因为在后面的push
操作中我们往vector中插入了i32
,所以Rust可以联系上下文推断出v
的类型是Vec
,因此我们也无需特别注明。
Dropping a Vector Drops Its Elements
像struct
一样,vector脱离作用域时,其元素所用的内存也会被释放。
读取Vector的元素
有两种方法:直接用下标或者用get
method,如:
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element is {}", third);
match v.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
有两个要注意的地方:
- 我们使用index
2
来获取第三个元素。(从0开始数) - 两种方法得到的都是引用类型。
此外,get
method的返回值是Some(&element)
或None
,也就是说,就算传递给它一个超越范围的index,也能通过编译并正常运行。
ところで、就像之前(https://zhuanlan.zhihu.com/p/86987925)所说,如下代码将会抛出一个CE:
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {}", first);
错误信息为:
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let first = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("The first element is: {}", first);
| ----- immutable borrow later used here
为什么对第一个元素的引用要关心vector的end的改变呢?这是因为vector的工作机制:加入一个新元素到vector尾时,如果此时已经分配到内存不足够用来加入新元素,则需要分配更多内存,这时vector就会索取一段新的内存,同时复制所有旧的元素到那个新的内存地址,所以旧的内存地址就会被废弃,此时对第一个元素的引用就会指向一段被废弃的地址,而这是不被Rust的内存保护机制允许的。
迭代遍历Vector的值
使用for
就可以实现这样的效果。且有两种方法。
第一种是获得不可变的引用:
let v = vec![100, 32, 57];
for i in &v {
println!("{}", i);
}
第二种是获得可变的引用:
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
为了改变可变引用指向的元素,我们需要使用dereference operator(*
)来获得i
指向的值。
(所以,使用println!("{}", *i);
也能实现与第一种相同的效果)
运用Enum来存储多种类型
在开始时我们就说过了,vector只能存储相同类型的值。但是如果我们想要存储多种类型的值时怎么办呢?答案就是运用Enum。如:
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
pop
pop
method可以移除vector的最后一个元素并且返回它,如:
println!("Pop last element: {:?}", xs.pop());
2 String
什么是String?
Rust的核心语言中只有一种string type——字符串切片str
。
而Rust的standard library提供了String
类型,
当我们谈到“strings”时,我们其实是同时指代了String
和&str
。而且这两者都是UTF-8编码的。
Rust的standard library里还包括了一些其他的string type,比如OsString
,OsStr
,CString
和CStr
。这些string type可以以不同的编码与在内存中的存储方式来存储文本。
创建一个String
很多对Vec
能进行的操作也能对String
进行,比如new
:
let mut s = String::new();
此外,也可以用to_string
method来创建一个有初始值的String
:
let data = "initial contents";
let s = data.to_string();
let s = "initial contents".to_string();
当然,也可以用之前用过很多次的String::from
:
let s = String::from("initial contents");
更新一个String
String
可以变长也可以改变内容,就像Vec
一样,可以"push"数据在它尾部。此外,也可以选择更加方便的+
运算符或者format!
宏来拼接String
值。
用push_str
和push
来append
我们可以用push_str
method来在String
后拼接上一个字符串切片,如:
let mut s = String::from("foo");
s.push_str("bar");
push_str
method的参数类型是一个字符串切片,因为我们不一定想要让它夺走ownership。
用+
运算符或format!
宏来拼接
如果想要组合两个已经存在的String
,可以使用+
运算符:
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1被move了,于是不能再被使用
之所以s1
在进行+
之后不再有效与s2
之前要加上引用符号,是因为当我们调用+
运算符时,它会使用add
method,像这样:
fn add(self, s: &str) -> String {
(这并不是add
在standard library里准确的亚子)
在add
函数里,我们只能在String
后加上一个&str
。但为什么我们在应该接受&str
的地方传递了一个&String
却仍然能通过编译呢?
那是因为Rust的编译器可以使&String
强制类型转换为&str
,使&s2
变成了&s2[..]
。
因为add
函数没有夺走s
参数的ownership,所以在进行运算后,s2
仍然有效。
如果我们想要拼接多个字符串,用+
就会显得很冗杂:
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;
不仅要输入一堆+
和"
,而且很难看清这行代码到底想做什么。
所以为了对付更加复杂的字符串组合,我们可以使用format!
宏:
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3);
format!
宏就像println!
一样运作,只不过它是返回一个String
而不是在屏幕上打印输出。
Indexing into Strings
在很多其他语言里,我们可以像这样来获取字符串中的某个字符:
let s1 = String::from("hello");
let h = s1[0];
但是如果在Rust中这样写,就会获得一个CE。Rust的字符串不支持indexing。为什么?为了回答这个问题,我们需要讨论一下Rust怎么在内存中存储字符串。
Internal Representation
String
就相当于一个Vec
,让我们看些UTF-8编码的例子:
let len = String::from("Hola").len();
在这个情况,len
是4,因为这个vector存储的“Hola”有4个字节长,每个字符占据了一个字节。
let len = String::from("Здравствуйте").len();
但是这个字符串的len
并非12,而是24:这是在UTF-8编码下 “Здравствуйте” 所占据的字节数,因为在这个字符串里的每个Unicode单值占据了内存的2个字节。因此,index就并不总是与一个Unicode单值相关,比如:
let hello = "Здравствуйте";
let answer = &hello[0];
当我们用UTF-8编码编码这个字符串时, З
的第一个字节是208
,第二个字节是151
,所以answer
实际上会相当于208
,但是208
本身并非一个有效字符。为了避免返回一个不想要的值从而造成我们不一定会很快发现的bug,Rust直接采取了不编译这段代码。
Bytes & Scalar Values & Grapheme Clusters
从Rust的视角来看待字符串有三种方法:bytes, scalar values, and grapheme clusters(离我们称作“字符”最接近的东西)。
让我们康康 “नमस्ते” 在Vec
中的存储方式:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
这总共是18字节,也正是计算机最终存储这个数据所采取的方式。
如果从Unicode scalar value,也就是Rust中的char
类型来看它,它就是:
['न', 'म', 'स', '्', 'त', 'े']
这总共是6个char
值,但是第四个和第六个其实并不是字母:它们是附加符号(diacritics),本身并没有意义。
如果从grapheme clusters来看,它就是印地语中的四个字母:
["न", "म", "स्", "ते"]
Rust能提供不同的表示字符串数据的方法,所以每个程序可以选择它所需要的那种表示方法,无论数据里存的是哪种语言。
另外一个Rust不允许我们index to a String
的原因是,index操作的时间复杂度被希望是(O(1)),但是在String
中并不能保证这样,因为Rust需要从头遍历字符串的内容来确认里面有多少个有效字符。
Slicing Strings
let hello = "Здравствуйте";
let s = &hello[0..4];
在上面的代码中,s
会是一个包含着4个字节的&str
,又因为这里每个字符都是2字节,所以s
是 Зд
。
但是如果我们写了&hello[0..1]
,则在运行中就会抛出一个RE:
thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`', src/libcore/str/mod.rs:2188:4
所以在玩字符串切片时要小心,因为它可能会crash恁的程序。
遍历字符串的方法
如果想要对单个Unicode scalar value做操作,最好的方法是使用chars
method。如:
for c in "नमस्ते".chars() {
println!("{}", c);
}
它会输出:
न
म
स
्
त
े
而bytes
method会返回每一个raw byte,如:
for b in "नमस्ते".bytes() {
println!("{}", b);
}
它会输出组成这个String
的那18个字节。
而standard library并不提供得到grapheme clusters的方法,可以去crates.io
找到相应的crate。
3 Hash Maps
类型HashMap
存储着一个从类型K
的Key到类型V
的Value的映射。
创建一个Hash Map
可以用new
来创建一个空的hash map,并且可以用insert
来加入元素。如:
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
在三种common collections里,hash map是使用频率最低的,所以它并不会自动地被包含在域里,所以我们需要首先使用use
来从std::collections
中引入它。
就像vector一样,hash map将数据存储在堆上。例子中的HashMap
有类型为String
的Key和类型为i32
的Value。就像vector一样,hash map中的所有key都应该是同一类型的,所有value也应该是同一类型的。
另外一种创建hash map的方法就是在一个vector of tuples上使用collect
method。如果我们有两个分别存储key和value的vector,我们可以使用zip
method来创建一个vector of tuples使它们两两对应地匹配,然后就可以使用collect
方法使其变为一个hash map。如:
use std::collections::HashMap;
let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
这里需要使用HashMap<_, _>
因为collect
可以生成很多collection类型,所以如果不指出我们需要的是hash map,Rust就不知道该返回哪个。在key和value的类型参数上我们用了_
,因为Rust可以从collect的vector中推断出hash map中要包含的类型。
Hash Maps & Ownership
对可Copy
的类型,比如i32
,只有它的值被复制到hash map里。但是对owned values比如String
,它的值会被move,从而hash map会变成这些值的owner,如:
use std::collections::HashMap;
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
// field_name与field_value不再有效
如果想要在生成hash map之后继续使用这些值,我们可以考虑使用它的引用来生成hash map。
获取Hash Map中的值
我们可以用get
method来用key获取hash map中对应的值。如:
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score = scores.get(&team_name);
在这个例子中,get
会返回Some(&10)
,返回值被包在Some
里是因为get
的返回值是Option<&V>
,如果hash map中没有该key对应的value,则会返回一个None
。因此我们还需要处理返回的Option
。
我们也可以用类似我们迭代遍历vector所采取的方法:
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
for (key, value) in &scores {
println!("{}: {}", key, value);
}
更新一个Hash Map
Overwriting a Value
如果我们想要插入一对key和value,hash map中又已经存在了那个key,并对应着不同的value,那么那个原来的value会被新的value给替换。
Only Inserting a Value If the Key Has No Value
hash map有一个特殊的API叫做entry
,它可以用来检测恁想要插入的key在hash map中存不存在并返回一个enum,如:
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
println!("{:?}", scores);
其中在Entry
后的or_insert
method,如果那个key在hash map中已经存在,则会返回对应的value的一个可变引用,如果不存在,则插入它的参数作为对应的value。
Updating a Value Based on the Old Value
另外一种常见的情况是查找一个key对应的value,然后依据这个value更新成新的value。如:
use std::collections::HashMap;
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", map);
这段代码会输出 {"world": 2, "hello": 1, "wonderful": 1}
,因为or_insert
method返回的是可变引用(&mut V
),所以我们可以用*
进行dereference,从而改变对应的值本身。
其中split_whitespace
就如同它的字面意思,可以根据空白符分割一个字符串并返回一个迭代器。
Hashing Functions
HashMap
所用的哈希函数并非最快的哈希函数,但是它提供了安全性。如果恁觉得它原来的哈希函数太慢了,恁可以用BuildHasher
将其换成其它哈希函数。此外,crates.io
中也有一些轮子。
小结
Vector、String和Hash Map可以给存储、获取、修改数据提供很大的方便。
standard library中还有不少Vector、String与Hash Map的method,值得一康。
参考
The Rust Programming Language by Steve Klabnik and Carol Nichols, with contributions from the Rust Community : https://doc.rust-lang.org/book/
Rust by Example:https://doc.rust-lang.org/rust-by-example/index.html