• 【Rust基础④】Rust中的集合类型(Vector与HashMap)


    8 集合类型

    8.1 动态数组 Vector

    动态数组允许你存储多个值,这些值在内存中一个紧挨着另一个排列,因此访问其中某个元素的成本非常低。

    8.1.1 创建动态数组

    let mut v1 = Vec::new(); 
    v.push(1); //自动推导类型为Vec
    
    let v2 = vec![1, 2, 3];
    
    • 1
    • 2
    • 3
    • 4

    结构体一样,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!("去你的第三个元素,根本没有!"),
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    若数组访问越界,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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    编译上面的代码,编译器会报错:

    $ 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    原因在于:数组的大小是可变的,当旧数组的大小不够用时,Rust 会重新分配一块更大的内存空间,然后把旧数组拷贝过来。这种情况下,之前的引用显然会指向一块无效的内存,这非常 rusty —— 对用户进行严格的教育。

    8.1.3 迭代遍历 Vector 中的元素

    如果想要依次访问数组中的元素,可以使用迭代的方式去遍历数组,这种方式比用下标的方式去遍历数组更安全也更高效(每次下标访问都会触发数组边界检查):

    let v = vec![1, 2, 3];
    for i in &v {
        println!("{}", i);
    }
    
    • 1
    • 2
    • 3
    • 4

    也可以在迭代过程中,修改 Vector 中的元素:

    let mut v = vec![1, 2, 3];
    for i in &mut v {
        *i += 10
    }
    
    • 1
    • 2
    • 3
    • 4

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    数组 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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    比枚举实现要稍微复杂一些,我们为 V4V6 都实现了特征 IpAddr,然后将它俩的实例用 Box::new 包裹后,存在了数组 v 中,需要注意的是,这里必须手动地指定类型:Vec>,表示数组 v 存储的是特征 IpAddr 的对象,这样就实现了在数组中存储不同的类型。

    在实际使用场景中,特征对象数组要比枚举数组常见很多,主要原因在于特征对象非常灵活,而编译器对枚举的限制较多,且无法动态增加类型。

    8.2 KV 存储 HashMap

    HashMap 中存储的是一一映射的 KV 键值对,并提供了平均复杂度为 O(1) 的查询方法,Rust 中哈希类型(哈希映射)为 HashMap

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    上面有几点需要注意:

    • get 方法返回一个 Option<&i32> 类型:当查询不到时,会返回一个 None,查询到时返回 Some(&i32)
    • &i32 是对 HashMap 中值的借用,如果不使用借用,可能会发生所有权的转移

    还可以通过循环的方式依次遍历 KV 对:

    for (key, value) in &scores {
        println!("{}: {}", key, value);
    }
    
    • 1
    • 2
    • 3

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    上面代码中,新建一个 map 用于保存词语出现的次数,插入一个词语时会进行判断:若之前没有插入过,则使用该词语作 Key,插入次数 0 作为 Value,若之前插入过则取出之前统计的该词语出现的次数,对其加一。

    有两点值得注意:

    • or_insert 返回了 &mut v 引用,因此可以通过该可变引用直接修改 map 中对应的值
    • 使用 count 引用时,需要先进行解引用 *count,否则会出现类型不匹配

    8.2.4 哈希函数

    一个类型能否作为 Key 的关键就是是否能进行相等比较,或者说该类型是否实现了 std::cmp::Eq 特征。

    f32 和 f64 浮点数,没有实现 std::cmp::Eq 特征,因此不可以用作 HashMapKey

    通过哈希函数可以把 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'转换为整数,97
    
       println!("{},{},{}",a,b,c)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    内存地址转换为指针:

    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类型占用4个字节,因此将内存地址 + 4
    let p2 = second_address as *mut i32; // 访问该地址指向的下一个整数p2
    unsafe {
        *p2 += 1;
    }
    assert_eq!(values[1], 3);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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
            }
        };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    运行后输出如下 "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

    再进一步,我们使用完全限定语法来进行准确的函数调用:

    1. 首先,编译器检查它是否可以直接调用 T::foo(value),称之为值方法调用
    2. 如果上一步调用无法完成(例如方法类型错误或者特征没有针对 Self 进行实现,上文提到过特征不能进行强制转换),那么编译器会尝试增加自动引用,例如会尝试以下调用: <&T>::foo(value)<&mut T>::foo(value),称之为引用方法调用
    3. 若上面两个方法依然不工作,编译器会试着解引用 T ,然后再进行尝试。这里使用了 Deref 特征 —— 若 T: Deref (T 可以被解引用为 U),那么编译器会使用 U 类型进行尝试,称之为解引用方法调用
    4. T 不能被解引用,且 T 是一个定长类型(在编译器类型长度是已知的),那么编译器也会尝试将 T 从定长类型转为不定长类型,例如将 [i32; 2] 转为 [i32]
  • 相关阅读:
    【Node.JS 】http的概念及作用
    【深入浅出设计模式--命令模式】
    企业制胜采购管理分别有哪五种策略?
    固定资产管理子系统报表分为什么大类,包括哪些科目
    数据结构之拓扑排序
    【微信小程序】一文解决button、input、image组件
    Docker基本配置及使用
    为什么spring默认采用单例bean
    图片批量处理:轻松实现图片批量处理:按需缩放图片像素
    Vue3中使用provide和inject依赖注入完成父组件和孙子组件之间参数传递
  • 原文地址:https://blog.csdn.net/qq_51601649/article/details/133786411