文章目录
- 8 集合类型
- 8.1 动态数组 Vector
- 8.1.1 创建动态数组
- 8.1.2 从 Vector 中读取元素
- 8.1.3 迭代遍历 Vector 中的元素
- 8.1.4 存储不同类型的元素
- 8.2 KV 存储 HashMap
- 8.2.1 创建 HashMap
- 使用 new 方法创建
- 使用迭代器和 collect 方法创建
- 8.2.2 查询 HashMap
- 8.2.3 更新 HashMap 的值
- 8.2.4 哈希函数
- 9 类型转换
- 9.1 as 转换
- 9.2 TryInto 转换
- 9.3 通用类型转换
8 集合类型
8.1 动态数组 Vector
动态数组允许你存储多个值,这些值在内存中一个紧挨着另一个排列,因此访问其中某个元素的成本非常低。
8.1.1 创建动态数组
let mut v1 = Vec::new();
v.push(1); //自动推导类型为Vec<i32>let v2 = vec![1, 2, 3];
跟结构体一样,Vector
类型在超出作用域范围后,会被自动删除
8.1.2 从 Vector 中读取元素
读取指定位置的元素有两种方式可选:
- 通过下标索引访问。
- 使用
get
方法。
let v = vec![1, 2, 3, 4, 5];let third: &i32 = &v[2];
println!("第三个元素是 {}", third);match v.get(2) {Some(third) => println!("第三个元素是 {}", third),None => println!("去你的第三个元素,根本没有!"),
}
若数组访问越界,v.get
在内部做了处理,有值的时候返回 Some(T)
,无值的时候返回 None
,因此 v.get
的使用方式非常安全。
let mut v = vec![1, 2, 3, 4, 5];let first = &v[0]; //不可变借用v.push(6); //可变借用println!("The first element is: {}", first);
编译上面的代码,编译器会报错:
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable 无法对v进行可变借用,因此之前已经进行了不可变借用
--> 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 // 不可变借用在这里被使用For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` due to previous error
原因在于:数组的大小是可变的,当旧数组的大小不够用时,Rust 会重新分配一块更大的内存空间,然后把旧数组拷贝过来。这种情况下,之前的引用显然会指向一块无效的内存,这非常 rusty —— 对用户进行严格的教育。
8.1.3 迭代遍历 Vector 中的元素
如果想要依次访问数组中的元素,可以使用迭代的方式去遍历数组,这种方式比用下标的方式去遍历数组更安全也更高效(每次下标访问都会触发数组边界检查):
let v = vec![1, 2, 3];
for i in &v {println!("{}", i);
}
也可以在迭代过程中,修改 Vector
中的元素:
let mut v = vec![1, 2, 3];
for i in &mut v {*i += 10
}
8.1.4 存储不同类型的元素
通过枚举实现:
#[derive(Debug)]
enum IpAddr {V4(String),V6(String)
}
fn main() {let v = vec![IpAddr::V4("127.0.0.1".to_string()),IpAddr::V6("::1".to_string())];for ip in v {show_addr(ip)}
}fn show_addr(ip: IpAddr) {println!("{:?}",ip);
}
数组 v
中存储了两种不同的 ip
地址,但是这两种都属于 IpAddr
枚举类型的成员,因此可以存储在数组中。
特征对象的实现:
trait IpAddr {fn display(&self);
}struct V4(String);
impl IpAddr for V4 {fn display(&self) {println!("ipv4: {:?}",self.0)}
}
struct V6(String);
impl IpAddr for V6 {fn display(&self) {println!("ipv6: {:?}",self.0)}
}fn main() {let v: Vec<Box<dyn IpAddr>> = vec![Box::new(V4("127.0.0.1".to_string())),Box::new(V6("::1".to_string())),];for ip in v {ip.display();}
}
比枚举实现要稍微复杂一些,我们为 V4
和 V6
都实现了特征 IpAddr
,然后将它俩的实例用 Box::new
包裹后,存在了数组 v
中,需要注意的是,这里必须手动地指定类型:Vec<Box<dyn IpAddr>>
,表示数组 v
存储的是特征 IpAddr
的对象,这样就实现了在数组中存储不同的类型。
在实际使用场景中,特征对象数组要比枚举数组常见很多,主要原因在于特征对象非常灵活,而编译器对枚举的限制较多,且无法动态增加类型。
8.2 KV 存储 HashMap
HashMap
中存储的是一一映射的 KV
键值对,并提供了平均复杂度为 O(1)
的查询方法,Rust 中哈希类型(哈希映射)为 HashMap<K,V>
。
8.2.1 创建 HashMap
使用 new 方法创建
使用 new
方法来创建 HashMap
,然后通过 insert
方法插入键值对。
use std::collections::HashMap;// 创建一个HashMap,用于存储宝石种类和对应的数量
let mut my_gems = HashMap::new();// 将宝石类型和对应的数量写入表中
my_gems.insert("红宝石", 1);
my_gems.insert("蓝宝石", 2);
my_gems.insert("河边捡的误以为是宝石的破石头", 18);
该 HashMap
的类型:HashMap<&str,i32>
。所有的集合类型都是动态的,意味着它们没有固定的内存大小,因此它们底层的数据都存储在内存堆上,然后通过一个存储在栈中的引用类型来访问。同时,跟其它集合类型一致,HashMap
也是内聚性的,即所有的 K
必须拥有同样的类型,V
也是如此。
跟
Vec
一样,如果预先知道要存储的KV
对个数,可以使用HashMap::with_capacity(capacity)
创建指定大小的HashMap
,避免频繁的内存分配和拷贝,提升性能
使用迭代器和 collect 方法创建
fn main() {use std::collections::HashMap;let teams_list = vec![("中国队".to_string(), 100),("美国队".to_string(), 10),("日本队".to_string(), 50),];let teams_map: HashMap<_,_> = teams_list.into_iter().collect();println!("{:?}",teams_map)
}
into_iter
方法将列表转为迭代器,接着通过 collect
进行收集,不过需要注意的是,collect
方法在内部实际上支持生成多种类型的目标集合,因此我们需要通过类型标注 HashMap<_,_>
来让编译器自己推导具体的KV类型。
HashMap
的所有权规则与其它 Rust 类型没有区别:
- 若类型实现
Copy
特征,该类型会被复制进HashMap
,因此无所谓所有权 - 若没实现
Copy
特征,所有权将被转移给HashMap
中
8.2.2 查询 HashMap
通过 get
方法可以获取元素:
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: Option<&i32> = scores.get(&team_name);
上面有几点需要注意:
get
方法返回一个Option<&i32>
类型:当查询不到时,会返回一个None
,查询到时返回Some(&i32)
&i32
是对HashMap
中值的借用,如果不使用借用,可能会发生所有权的转移
还可以通过循环的方式依次遍历 KV
对:
for (key, value) in &scores {println!("{}: {}", key, value);
}
8.2.3 更新 HashMap 的值
查询某个 key
对应的值,若不存在则插入新值,若存在则对已有的值进行更新,例如在文本中统计词语出现的次数:
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);
上面代码中,新建一个 map
用于保存词语出现的次数,插入一个词语时会进行判断:若之前没有插入过,则使用该词语作 Key
,插入次数 0 作为 Value
,若之前插入过则取出之前统计的该词语出现的次数,对其加一。
有两点值得注意:
or_insert
返回了&mut v
引用,因此可以通过该可变引用直接修改map
中对应的值- 使用
count
引用时,需要先进行解引用*count
,否则会出现类型不匹配
8.2.4 哈希函数
一个类型能否作为 Key
的关键就是是否能进行相等比较,或者说该类型是否实现了 std::cmp::Eq
特征。
f32 和 f64 浮点数,没有实现
std::cmp::Eq
特征,因此不可以用作HashMap
的Key
通过哈希函数可以把 Key
计算后映射为哈希值,然后使用该哈希值来进行存储、查询、比较等操作。
因此若性能测试显示当前标准库默认的哈希函数不能满足你的性能需求,就需要去 crates.io
上寻找其它的哈希函数实现
9 类型转换
9.1 as 转换
转换时,把范围较小的类型转换为较大的类型,下面列出了常用的转换形式:
fn main() {let a = 3.1 as i8;let b = 100_i8 as i32;let c = 'a' as u8; // 将字符'a'转换为整数,97println!("{},{},{}",a,b,c)
}
内存地址转换为指针:
let mut values: [i32; 2] = [1, 2];
let p1: *mut i32 = values.as_mut_ptr();
let first_address = p1 as usize; // 将p1内存地址转换为一个整数
let second_address = first_address + 4; // 4 == std::mem::size_of::<i32>(),i32类型占用4个字节,因此将内存地址 + 4
let p2 = second_address as *mut i32; // 访问该地址指向的下一个整数p2
unsafe {*p2 += 1;
}
assert_eq!(values[1], 3);
9.2 TryInto 转换
try_into
会尝试进行一次转换,并返回一个 Result
,此时就可以对其进行相应的错误处理。
try_into
转换会捕获大类型向小类型转换时导致的溢出错误:
fn main() {let b: i16 = 1500;let b_: u8 = match b.try_into() {Ok(b1) => b1,Err(e) => {println!("{:?}", e.to_string());0}};
}
运行后输出如下 "out of range integral type conversion attempted"
,在这里我们程序捕获了错误,编译器告诉我们类型范围超出的转换是不被允许的,因为我们试图把 1500_i16
转换为 u8
类型,后者明显不足以承载这么大的值。
9.3 通用类型转换
在某些情况下,类型是可以进行隐式强制转换的,虽然这些转换弱化了 Rust 的类型系统,但是它们的存在是为了让 Rust 在大多数场景可以工作,而不是报各种类型上的编译错误。
在匹配特征时,不会做任何强制转换(除了方法)。一个类型 T
可以强制转换为 U
,不代表 impl T
可以强制转换为 impl U
点操作符在调用时,会发生很多魔法般的类型转换,例如:自动引用、自动解引用,强制类型转换直到类型能匹配等。
假设有一个方法 foo
,它有一个接收器(接收器就是 self
、&self
、&mut self
参数)。如果调用 value.foo()
,编译器在调用 foo
之前,需要决定到底使用哪个 Self
类型来调用。现在假设 value
拥有类型 T
。
再进一步,我们使用完全限定语法来进行准确的函数调用:
- 首先,编译器检查它是否可以直接调用
T::foo(value)
,称之为值方法调用 - 如果上一步调用无法完成(例如方法类型错误或者特征没有针对
Self
进行实现,上文提到过特征不能进行强制转换),那么编译器会尝试增加自动引用,例如会尝试以下调用:<&T>::foo(value)
和<&mut T>::foo(value)
,称之为引用方法调用 - 若上面两个方法依然不工作,编译器会试着解引用
T
,然后再进行尝试。这里使用了Deref
特征 —— 若T: Deref<Target = U>
(T
可以被解引用为U
),那么编译器会使用U
类型进行尝试,称之为解引用方法调用 - 若
T
不能被解引用,且T
是一个定长类型(在编译器类型长度是已知的),那么编译器也会尝试将T
从定长类型转为不定长类型,例如将[i32; 2]
转为[i32]