通用集合类型
集合包含多个值,这些集合将自己持有的数据存储在堆上。
本章讨论下面三个集合
- 动态数组(vector):连续的存储多个值
- 字符串(String)是字符的集合
- Hash map 将值关联到一个特定的键上
使用动态数组储存多个值
创建动态数组
Vec<T>
可以储存T
类型的
调用函数Vec::new
来创建一个空动态数组
let v:Vec<i32> = Vec::new();
或者使用vec!
宏来根据提供的初始值创建动态数组
let v = vec![1,2,3];
更新动态数组
使用push方法将新的元素添加到动态数组
let mut v = Vec::new();
v.push(5);
销毁动态数组时也会销毁其中的元素
动态数组一离开作用域就会被销毁,其内部的所有内容也会被销毁
读取动态数组的元素
使用索引
let third:&i32 = &v[2];
注意这里使用的引用
如果读取到非法的位置,会引起一个panic
get方法
match v.get(2) {
Some(third) => println!("The third element is {}",third),
None => println!("There is no third element"),
}
该方法返回一个Option<T>
,如果数组越界,返回None
不成功的引用案例
let mut v = vec![1,2,3];
let firts = &v[0];
v.push(6);
编译器报错的是:在firtt这里声明了不可变引用,在v.push这里使用了可变引用
进一步思考内涵:vector的实现问题。当进行扩容时,会重新分配内存,此时原引用有可能成为悬垂引用
遍历动态数组中的值
使用for循环获得数组中的每个元素的引用
不可变、可变都可以
let mut v = vec![1,2,3];
for i in &mut v {
*i += 1;
}
使用枚举储存多个类型的值
一个小技巧,如果想在动态数组中储存不同类型的值,可以将这几个类型先打包成枚举
enum IntFloat {
Int(i32),
Float(f64),
}
let v = vec![IntFloat::Int(32),IntFloat::Float(1.2)];
使用字符串存储UTF-8编码的文本
创建字符串
空串:
let mut s = String::new();
根据字符串字面量创建
let s = String::from("hello");
对于实现了Display trait 的类型调用to_string()
let s = "hello".to_string();
更新字符串
使用push_str或push向字符串添加内容
push_str方法将一个字符串切片中的内容添加到字符串后
s.push_str("str");
push方法将单个字符添加到字符串末尾
s.push('哈');
使用+运算符或者format!宏拼接字符串
+运算符会调用一个类似fn add(self,s:&str) -> String
的方法。简单来说该方法是取得一个String类型的所有权,将一个&str添加到后面,然后返回该String变量
let s1 = "hello".to_str();
let s2 = " world".to_str();
let s3 = s1 + &s2;//这样之后,s1的所有权转移,s1这个名字不再有效
注意,此处使用一种称为 解引用强制转换 的技术,将&s2
强制转换成&s2[..]
对于复杂的字符串合并,使用format!宏
let s1 = "hello".to_str();
let s2 = "world".to_str();
let s3 = format!("{} {}!",s1,s2);
其使用方法和println!宏是一样的
这里也不会取得参数的所有权
字符串索引
字符串不支持使用索引,原因与字符串的内存布局有关
内存布局
字符串实际上是基于Vec<u8>
的封装类型,但是并不是一个字符占据一个字节。
对于有ASCII码的字符,占据一个字节;对于其它字符,比如汉字,占据两个字节
所以在使用索引时,很可能只是索引到某个字符的“一半”
更详细的解释可以看这篇知乎文章UTF-8编码
字符串切片
指定所需要的字节内容
let hello = "你好";
let s = &hello[0..2];
这样就是取出来了“你”
。另外,如果我们尝试用let s = &hello[0..1]
取“汉字的一半”,就会在运行时发生panic
遍历字符串的方法
对字符串中的每一个unicode标量值进行处理,使用chars()
方法
for c in "你好".chars() {
println!("()",c);
}
对字符串内存中的每个原始字节进行处理,使用bytes
方法
for c in "你好".bytes() {
println!("()",c);
}
在HashMap中储存键值对
HashMap<K,V>
储存了从K类型键到V类型值之间的映射关系
创建一个新的HashMap
需要使用use将HashMap引入作用域
使用new方法创建一个新的哈希映射
使用insert方法来插入键值对
use std::collections::HashMap;
let mut scores:HashMap<String,i32> = HashMap::new();
scores.insert(String::from("A"),10);
scores.insert(String::from("B"),20);
使用zip方法,配合iter(),collect(),来将两个动态数组合并成一个HashMap。
下面这个例子获得和上面例子一样的HashMap
use std::collections::HashMap;
let team = vec![String::from("A"),String::from("A")];
let sc = vec![10,20];
let scores:HashMap<_,_> =
teams.iter().zip(sc.iter()).collect();
这里HashMap<_,_>
不能省略
- 一方面,collect可以用于不同数据结构,必须指明是
HAshMap
- 一方面,编译器可以自动推断出键值的类型,所以只用
_
占位即可
HashMap与所有权
- 对于实现了 copy trait 的类型,他们的值会简单的复制到HashMap中
- 对于持有所有权的值,其值会转移,所有权会转交给HashMap
- 将值的引用插入哈希映射,这些引用指向的值必须保证在哈希映射有效时也是有效的,这通过生命周期实现
访问哈希映射中的值
将值传入get方法来获得值
- 返回一个Option<&T>
- 存在这个键值对,返会
Some(T)
- 不存在,返回
None
- 存在这个键值对,返会
let team_name = "A".to_str();
let score = scores.get(&team_name);
match score {
//...
//...
}
使用for循环遍历所有键值对
for (key,value) in &scores {
//...
}
遍历的顺序是随机的
更新哈希映射
覆盖旧值
将已有的键并配以不同的值来继续拆入,新的值就会覆盖旧的值
scores.insert(String::from("C"),10);
scores.insert(String::from("C"),20);
检测键值对存在性并插入数据
检查一个键是否存在对应值,不存在,为其插入一个值
使用entry API 实现
- entry方法返回一个Entry枚举类型来指明键所对应的值是否存在
- Entry类型的or_insert方法定义为返回一个Entry键所指向值的可变引用
- 如果这个值不存在,就将参数作为新值插入到HashMap中
scores.entry(String::from("C")).or_insert(10);
依据旧值来更新新值
仍然是使用entry
比如统计单词出现次数
use std::collections::HashMap;
let text = "hello world world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0); //若是新单词,初始化为0
*count += 1; //对于以出现过的单词,计数加一
}
哈希函数
为了提供抵御拒绝服务攻击(DoS, Denial of Service)的能力, HashMap默认使用了一个在密码学上安全的哈希函数
你也可以通过指定不同的哈希计算工具来使用其他函数。这里的哈希计算工具特指实现了BuildHasher trait的类型