0、背景

  • 集合指向的数据存在堆上,不需要在编译器知道数据数量

    1、vector

    存储相同类型的可变数量的值

    基本操作

    新建

  • 直接使用关联方法new新建,需要手动标注类型,因为没有数据来做类型推断

  • 使用初始值来创建,不需要指定类型,利用 vec! 宏,间接调用了new

    1. let v1: Vec<i32> = Vec::new();
    2. let v2 = vec![1, 2, 3];

    更新

  • push

  • 需要标注vec可变mut
  • 下面的实例中Rust可以根据push的数据做出类型推断,不需要手动标注类型

    1. let mut v = Vec::new();
    2. v.push(3);

    丢弃vector

  • vec离开作用域时会被释放,其内部元素也会被丢弃

    1. {
    2. let v = vec![1, 2, 3, 4];
    3. // 处理变量 v
    4. } // <- 这里 v 离开作用域并被丢弃

    读取vector元素

    有两种方式

  • 索引:let x: &i32 = &v[2]; 返回一个引用

  • get:返回一个Option<&T>

    为什么要两种方式

  • 让程序员选择如何处理索引值在vector中没有对应值的情况

  • 直接使用索引时,访问不存在元素会造成panic:当越界访问是个大问题时,最好让程序崩溃
  • 使用get时,越界访问不会panic而是返回None:偶尔出现越界访问很正常时可以用get

    • 但是我代码要加上处理Some(&T)和None的逻辑
    • 比如用户输入太大时,我处理None时可以告诉用户他的输入太大
      1. let v = vec![1, 2, 3];
      2. let not_exist = &v[100];
      3. let noet_exist = v.get(100);

      看似离谱的borrow checker

  • 访问vec元素时用到了引用,既然有引用,那么就得检查所有权和借用规则

    • 为什么呢?因为要保证对vector中元素的引用要和其他引用保持有效
  • 下面的例子为啥就编译不过呢?好离谱。。。
    • vec底层实现时,增加新元素时,没足够空间存放的话,可能会申请全新的、更大的内存空间,并将原vec中的内容拷贝过去,同时此时会析构原来的空间,而下面代码中引用的 &v[0] 指向被释放的内存
    • 一想到borrow checker连这个都能查出来,头皮发麻
      1. let mut v = vec![1, 2, 3];
      2. let first = &v[0]; //immut borrow
      3. v.push(6); //mut borrow
      4. print!("first element is {}", first); //immut borrow used

      遍历vec

      1. for i in &v {} //遍历不可变引用
      2. for i in &mut v { *i+=5;} //遍历可变引用和解引用

      使用枚举来在vec中存储多种类型

      内心os:啊这,啊这,啊这
      场景:电子表格中,一行可能有数字、字符串很多类型,但我想针对单元格统一不同类型,定义一个存不同类型值的枚举,于是把很多实际的类型当成统一的类型来看待 ```rust enum SpreadsheetCell { Int(i32), Float(f64), Text(String), }

let row = vec![ SpreadsheetCell::Int(3), SpreadsheetCell::Text(String::from(“blue”)), SpreadsheetCell::Float(10.12), ];

  1. <a name="xZtJs"></a>
  2. # 2、String
  3. 字符串是字符集合,有集合类型的通用操作<br />字符串和其他集合不同,它很复杂
  4. <a name="RdaLp"></a>
  5. ## 什么是字符串
  6. - **Rust核心语言中只有一种字符串类型 str**
  7. - 字符串slice,以借用形式&str,是对存在别处的字符串数据的引用
  8. - String是标准库提供的,不在Rust核心语言中
  9. - 是可变的、有所有权的、UTF-8的字符串类型
  10. - String和字符串slice都是UTF-8编码
  11. - 标准库还有其他字符串类型 OsString OsStr CString等
  12. - 后缀String或Str对应他们提供的所有权,String是拥有所有权的。String需要额外的堆空间来存储,str不需要
  13. - 给C API传字符串时不能用Rust字符串,而是CString,是不存储长度,以0结尾的字符序列
  14. <a name="lkxlU"></a>
  15. ## 新建字符串
  16. - String::from 和 .to_string做完全相同工作,选啥是风格问题
  17. ```rust
  18. let data = "data";
  19. let s = data.to_string();
  20. //同上方法
  21. let s = "data".to_string();
  22. //同上方法
  23. let s = String::from("data");
  24. let mut s = String::new(); //新建空串

附加和拼接字符串

  • 附加的push_str函数采用的是字符串slice,并不需要将所有权trap进去,因此下面的s2还能使用

    1. let mut s1 = String::from("foo");
    2. let s2 = "bar";
    3. s1.push_str(s2);
    4. println!("s2 is {}", s2);
  • push方法用于单个字符:s.push(‘l’)

拼接字符串

  • +运算符使用add函数,于是函数签名中后一个数会使用引用
  • 根据签名,我们只能将&str和String相加,不能将两个String相加
  • 问题:s2是&String,签名里加的是我&str,和&String有什么关系?
    • &String可以被强转成&str
    • add调用时,Rust使用解引用强制多态
    • 可以理解为 &s2 变成了 &s2[..]
  • s1+&s2 中

    • s1的所有权陷入了add的self中
    • s2用引用,于是s2还能继续用
    • 整个语句,首先获得s1所有权,再通过引用从s2拷贝,最终返回结果的所有权
      1. fn add(self, s: &str) -> String {}
      2. let s1 = String::from("Hello, ");
      3. let s2 = String::from("world!");
      4. let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用
      使用format!替代多次字符串拼接
  • 参数的所有权都不会陷入进来

  • 白拿一个结果字符串s,美滋滋

    1. let s1 = String::from("tic");
    2. let s2 = String::from("tac");
    3. let s3 = String::from("toe");
    4. let s = format!("{}-{}-{}", s1, s2, s3);

    索引字符串

    Rust字符串不支持索引!!!
    为什么不支持索引呢????——UTF-8编码的问题

  • 普通字符每一个字母UTF-8编码都是一个字节,Unicode标量值需要两个字节存储,于是索引在这里会有歧义,大家都不希望Unicode只返回一半 ```rust let len = String::from(“Hola”).len(); //看上去4,实际4 let len = String::from(“Здравствуйте”).len(); //看上去12,实际24

let hello = “Здравствуйте”; let answer = &hello[0];//看上去是3,实际上UTF-8编码中对应两个字节,返回的是第一个字节208

  1. **索引字符串总是不好的,因为索引应该返回的类型不明确**
  2. - 字节值
  3. - 字符
  4. - 字形簇
  5. - 字符串slice
  6. **如果真想要索引字符串,你需要表达出诚意,如下**
  7. ```rust
  8. let hello = "Здравствуйте";
  9. let s = &hello[0..4]; //s的类型是&str,包含前四个字节,即“Зд”
  10. let s1 = &hello[0..1]; //这种情况会panic

老实人遍历字符串的做法

  • 需要指定遍历的每个对象,是操作单个Unicode,还是操作原始字节

    1. for c in "नमस्ते".chars() { //操作单个Unicode
    2. println!("{}", c);
    3. }
    4. for b in "नमस्ते".bytes() { //操作单个字节
    5. println!("{}", b);
    6. }

    3、哈希 map 存储键值对

    基本操作

    新建

  • HashMap没有prelude自动引用

  • 下面是常规操作

    1. use std::collections::HashMap;
    2. fn main() {
    3. let mut scores = HashMap::new();
    4. scores.insert(String::from("Blue"), 10);
    5. }

    使用zip+collect方法从两个vector创建

  • 用zip创建一个元组的vector

  • 用collect将元组的vector转成HashMap
  • 注意HashMap<_,_>类型注解是必要的,因为可能collect很多不同的数据结构,需要显式指定我要的数据结构,而key和value的类型可以推断

    1. let teams = vec![String::from("lpc"), String::from("pcl")];
    2. let numbers = vec![10, 46];
    3. let pairs: HashMap<_,_> = teams.iter().zip(numbers.iter()).collect();

    map的所有权

  • i32这种实现了Copy trait的类型可以直接拷贝

  • String这种拥有所有权的值,map会称为它们的所有权

    • 这些有所有权的值,在map有效的时候,会是有效的
      1. let name = String::from("name");
      2. let value = String::from("value");
      3. let mut map = HashMap::new();
      4. map.insert(name, value);
      5. println!("{}", name);//错误

      访问map中的值

      get方法直接根据key获取value
  • 返回Option:有结果,返回Some;没结果,返回None

  • 使用引用key参数,不会把所有权陷入进去

遍历map每一个键值对

  1. let score = scores.get(&team_name);
  2. for (key, value) in &scores {
  3. println!("{}: {}", key, value);
  4. }

更新map

  • 覆盖一个值:直接用相同的key来insert不同的value

    1. scores.insert(String::from("Blue"), 10);
    2. scores.insert(String::from("Blue"), 25); //上面的10被覆盖了
  • key没有对应value时插入:entry函数

    • entry函数返回值是一个枚举Entry,代表了可能存在也可能不存在的值
    • Entry的or_insert方法,在key存在时返回可变引用;不存在时则将参数作为新值插入,并返回新的可变引用
      1. scores.entry(String::from("Blue")).or_insert(50);
  • 根据旧值更新一个值

    • wordcount例子
      1. let text = "hello world";
      2. let mut map = HashMap::new();
      3. for word in text.split_whitespace() {
      4. let count = map.entry(word).or_insert(0); //count是可变引用 &mut i32
      5. *count += 1;
      6. }