除去前面文章里写的常用类型之外,还有一些很常用的集合类型( collections )。
Vector
Vector 是 Rust 中用来存储大小可变且同一数据类型的列表,也就是可以操作内容的 Array ( 关系有点像 String Literal 跟 String )。
创建
let mut v: Vec<i32> = Vec::new();v.push(1);v.push(2);
也可以用宏的写法
let mut v = vec![1, 2, 3];v.push(1);
并且可以通过 for in 对 vector 遍历并且对值进行操作。
let mut v = vec![1, 2, 3];for i in &mut v {*i += 1; // *i 的 * 是 dereference 操作符,因为 i 是引用,需要拿到值来进行操作,就需要用 *}
如果真的有需要存储多种类型的需求,可以用 Enum 来存储多种类型。
#[derive(Debug)]enum MyEnum {A(String),B(u32),C(f32),}let v3 = vec![MyEnum::A(String::from("123")),MyEnum::B(1),MyEnum::C(2.0),];println!("{:?}", v3);
读取
读取 vector 里的元素有两种写法,一种是 [] 一种是 .get ,前者返回的是非空值,如果查的 index 大于 vector 的长度,执行会报错,后者返回的是 Option<T>,可以用 match来判断是否非空。也就是如果确定查的 index 肯定是有值的,就可以用 [] ,如果不确定返回的是否非空,就用 .get 会更安全。
let v4 = vec![1, 2, 3];println!("{}", &v4[0]);match v4.get(0) {Some(n) => {println!("{}", n);}None => ()}
String
String 在 Rust 中是一个很复杂的存在,所以需要单拎出来讲,String 在 Rust 内部是通过 Vector 实现的,在 Vec<u8> 之上包了一层,实际存储的是 bytes 。
创建
String 有几种创建方式
let s = String::new(); // 空的 Stringlet s2 = String::from("hello"); // 用 hello 初始化了的 Stringlet s3 = "hello".to_string(); // 将 String literal 转成 String
更新
String 的更新主要有 push 和 push_str 两个方法,前者 push 的是 char ,后者是 String 。
let mut s = String::from("hello");s.push(',');s.push_str("world");
合并
多个 String 的合并,直接用 format! 这个宏即可。
宏的入参都会自动使用引用,所以 ownership 不会被转移。
let s = String::from("hello");let s2 = String::from(",");let s3 = String::from("world");let s4 = format!("{}{}{}", s, s2, s3);
为啥不用 +,因为 +在 String 里的实现,其实就是调用 add 方法,而 add 方法会转移 ownership ,比如下面这个例子。
let s = String::from("hello");let s2 = String::from(",world");let s3 = s + &s2;
为啥 s2 要用引用,前面说了,+这个操作符其实就是调用 s 的 add方法,这个方法的入参就是引用,所以 + 后面的需要是引用,然后因为 add 方法会转移 ownership ,所以上面的逻辑执行完后,s 就 invalid 了。如果要合并更多的 String ,都用 + 的话就会变得有点不可控,所以还是尽量用 format!这个宏。
读取
很多时候可能会需要读取字符的某个字段,在其他语言中一般就是直接用 []可以读取到,但是在 Rust 中是不支持这么读取的( 也就是 s[0] 这种写法是不允许的 )。
原因就是 String 的底层是 Vec ,Rust 认为通过 s[0] 这种方式读取到的一般不会是用户需要的,因为返回的是 byte ,所以干脆不给用户这么用了( 其实 Rust 里的解释有很长一大段 )。
但是支持 slicing ,也可以通过 chars 来获取,比如获取首字母,但是!!!两种都会有坑。
let s = String::from("hello world");// 通过 slicing 获取let h = &s[0..1];println!("{}", h);// 通过 chars 获取for (i, c) in s.chars().enumerate() {if i == 0 {println!("{}", c);} else {break;}}
因为案例里的代码写的是英文字符,一个字符就是一个 byte,所以上面 slicing 的方式没毛病,但是如果换成中文,因为中文并不是 1 个 byte ,那么 &s[0..1]就会抛 byte index 1 is not a char boundary 的异常,通过 chars 的方式就没问题。
但是 chars 也不是完全没问题,有部分字符,比如 नमस्ते 这种,chars 会解析成六个
नमस्ते
然后 Rust 文档里也没有给出最完美的解法,就说这个太复杂所以没有提供标准库来解决这个问题,让我们去 crates.io 上找开源的库解决,emmm…
HashMap
HashMap 就是提供 K-V 格式的 collections ,跟 Vector 一样,key 必须是同个类型,value 也必须是同个类型( 如果存在不同类型的场景,通过 Enum 解决 )。
创建 & 更新
注意,HashMap 的 insert 会转移 ownership 。
let mut h = HashMap::new();h.insert(String::from("h"), 1);println!("{:?}", h);
更新的话,只要再调一下 insert并且传入同样的 key 即可,会自动覆盖。
也可以通过两个 Vector 转换成 HashMap
let k = vec![String::from("h"), String::from("b")];let v = vec![1, 2];let mut h2: HashMap<_, _> = k.into_iter().zip(v.into_iter()).collect();println!("{:?}", h2);
将两个 vector 转成 iterator ,再通过 zip 将两个 iterator 合并起来再调用 collect 进行转换,需要申明 h2 的类型是 HashMap ,collect 方法才知道该转换成什么样的类型。之所以 HashMap 的两个泛型都用 _ 是因为 collect 可以自己推导出类型,不需要自己写。
使用 HashMap 的时候,可能会存在一种“如果没有则插入”的场景,这种场景可以通过 entry 来解决,比如拿上面的 h2 来操作。entry 可以返回一个 Entry实例,提供了 or_insert 等方法用于当无记录时做一些操作。
or_insert 会转移 Entry 实例的 ownership ,所以不能保存 entry 然后多次操作 or_insert 。
// 如果 hashmap 里没有 h 这个 key ,则 insert value 为 0。let r2 = h2.entry(String::from("h")).or_insert(0);*r2 += 1; // 对原始 value 进行 +1 操作println!("{}", r2);
判断某个实例的方法会不会转移 ownership ,看定义的第一个参数,如果是 &self 则说明不会转移,如果是 self 说明不是引用所以会转移 ownership ,这样代表只能执行一次,之后该实例就 invalid 了。
读取
可以通过 get 读取,返回的是 Option<T>,同样用上面的 h2 做例子。
let r = h2.get(&String::from("h"));match r {Some(n) => {println!("{}", n);},_ => (),}
也可以用 for in 遍历 HashMap
for (key, value) in &h2 {println!("{}: {}", key, value);}

