Rust基础 常见容器

Rust基础 常见容器,第1张

Rust基础 常见容器

文章目录
  • 常见容器
    • 向量`Vec`
      • 创建向量
      • 添加元素
      • 读取元素
      • 向量元素的引用和向量的引用
      • 删除向量
      • 使用迭代器访问元素
    • 字符串`String`
      • 创建字符串
      • 更新字符串
      • 读取字符串中的元素
      • 字符串切片
    • 哈希表`HashMap

常见容器

这部分主要介绍Rust中的向量Vec字符串和哈希表。

向量Vec

向量和数组的最大区别在于,数组的长度必须在编译时确定,而且之后长度不能更改,所以数组使用的是栈内存,而向量的长度是动态的,可以在运行时增加或减少,因此使用的是堆内存。

创建向量
fn main() {
    let vec: Vec = Vec::new();
}

这种创建方式必须指定向量类型。

fn main() {
    let vec = vec![1, 2, 3];
    let vec = vec![0; 5]; // [0, 0, 0, 0, 0]
}

这种创建方式可以根据数据自动推断出向量类型。

添加元素
fn main() {
    let vec: Vec = Vec::new();
    vec.push(0);
}
读取元素

直接利用索引读取,如下:

fn main() {
    let vec = vec![1, 2, 3, 4];
    println!("{}", vec[3]);
}

当访问越界时,比如vec[4],在运行时会panic。也可以使用get方法读取,它返回的是Option类型,如果访问越界,返回的是None,否则返回的是Some,因此需要结合match {}进行处理。

fn main() {
    let vec = vec![1, 2, 3, 4];
    let index = 4;
    match vec.get(index) {
        Some(v) => println!("{}", v),
        None => println!("None"),
    }
}
向量元素的引用和向量的引用

Vec的push方法需要修改向量,因此push方法的参数是&mut self,如果获得了向量中元素的引用,那么就不再允许调用push方法,因为会违背引用的规则:任意时刻只能存在一个可变引用或者多个不可变引用。

fn main() {
    let mut vec = vec![1, 2, 3, 4];
    let ele = &vec[3];
    vec.push(5); 
}

// 报错信息
// 3 |     let ele = &vec[3];
//   |                --- immutable borrow occurs here
// 4 |     vec.push(5); 
//   |     ^^^^^^^^^^^ mutable borrow occurs here
// 5 |     println!("{}", ele);
//   |                    --- immutable borrow later used here

如果向量中的元素没有实现Copy特性,比如Vec,那么let x = vec[0];会移动向量中元素的所有权,导致移动后vec[0]就会不存在,而Rust不允许这种情况出现,如下:

fn main() {
	let vec = vec![String::from("hello"), String::from("world")];
	let x = vec[0];
}

// 报错信息
// 3 |     let x = vec[0];
//   |             ^^^^^^
//   |             |
//   |             move occurs because value has type `String`, which does not implement the `Copy` trait
//   |             help: consider borrowing here: `&vec[0]`

如提示,可以通过引用访问元素,let x = &vec[0];,也可以通过get方法访问元素,这种情况下,返回的是引用,如下:

fn main() {
	let vec = vec![String::from("hello"), String::from("world")];
	let x = vec.get(0); // x是&String类型 
}
删除向量

当向量离开作用域后,会调用drop方法释放向量占用的资源,同时,向量中的元素也会调用drop方法释放资源。

使用迭代器访问元素

如果不改变向量中的元素,如下:

fn main() {
    let vec = vec![1, 2, 3, 4];
    for e in vec {
        print!("{}, ", e);
    }
    println!("");
}

// 输出
// 1, 2, 3, 4,

如果改变向量中的元素,如下:

fn main() {
    let vec = vec![1, 2, 3, 4];
    for mut e in vec {
        e += 1;
        print!("{}, ", e);
    }
    println!("");
}

// 输出
// 2, 3, 4, 5,

当执行for e in vec {}时,会自动调用vec的iter方法,返回一个迭代器,迭代器调用next方法获取下一个元素,返回Some或None,当返回Some时,会将数据提取出来赋给e,如果返回None,则遍历完成,退出。类似地,当执行执行for mut e in vec {}时,会自动调用vec的into_iter方法,into_iter和iter的区别见博客。而iter和into_iter方法,第一个参数都是self,也就是说执行方法后会发生所有权的转移,在执行for e in vec {}之后,vec就不能使用了,示例如下:

fn main() {
    let vec = vec![String::from("hello"), String::from("world")];
    for s in vec {
        println!("{}", s);
    }
    println!("{:?}", vec);
}

// 报错信息
// 2   |     let vec = vec![String::from("hello"), String::from("world")];
//     |         --- move occurs because `vec` has type `Vec`, which does not implement the `Copy` trait
// 3   |     for s in vec {
//     |              ---
//     |              |
//     |              `vec` moved due to this implicit call to `.into_iter()`
//     |              help: consider borrowing to avoid moving into the for loop: `&vec`
// ...
// 6   |     println!("{:?}", vec);
//     |                      ^^^ value borrowed here after move

还需要注意的是,vec中元素的所有权,也在每一次迭代中移动到了变量e上。

如果想要在使用迭代器访问vec元素后,依然可以使用vec,代码如下:

fn main() {
    let vec = vec![1, 2, 3, 4];
    for r in &vec {
        print!("{}, ", r);
    }
    println!("");
}

// 输出
// 1, 2, 3, 4,

并且想改变vec中元素的值,代码如下:

fn main() {
    let mut vec = vec![1, 2, 3, 4];
    for r in &mut vec {
        *r += 1;
        print!("{}, ", r);
    }
    println!("");
	println!("{:?}", vec)
}

// 输出
// 2, 3, 4, 5,
// [2, 3, 4, 5]

for r in &vec {}编译时,发现&Vec没有iter方法,会自动调用Deref特性实现的deref方法,将&Vec类型转换为&[i32]类型(切片),再由&[i32]类型调用iter方法,得到迭代器,而这个迭代器返回的是对其中元素的引用,for r in &mut vec {}的过程与之类似,不过返回的是可变引用。也就是说,如果是for r in &vec {} ,则r是vec元素的引用,如果是for r in &mut vec {},则r是vec元素的可变引用,需要先解引用再使用,比如*r += 1;。这些过程中隐式的方法调用的参数,都是&self或&mut self,迭代器的返回值也是引用类型,不论是vec还是位于其中的元素,都不会发生所有权的转移。

综上,for mut e in vec {}中mut在e前面,是因为e获取了vec中元素的所有权,因此想要e可变,需要用mut关键字修饰它,而for r in &mut vec {}中,r获取的是vec中元素的可变引用,由于我们只需要改变r指向的对象,而不需要改变r本身,所用不需要用mut修饰r。

字符串String

String、str和&str这三者的联系和区别,可见博客。

简单来说,String类型包含一个指向堆的指针,以及长度和容量信息,&str包含一个指向堆的指针和长度信息,str类型是被&str指向的数据,实际中,基本不会用到str,常用的是&str。

创建字符串

有以下几种方式:

  • 使用String::new()创建一个空的字符串。
  • 使用String::from("...")创建一个字符串。
  • 在实现了Display特性的对象上调用to_string方法得到一个字符串。
fn main() {
	let s = String::new();
	let s = String::from("hello, world");
    let n = 3;
    let s = n.to_string();
}
更新字符串
  • push方法,其参数是char类型。
  • push_str方法,其参数是&str类型。
  • let s3 = s1 + &s2;,调用字符串的add方法,其签名为fn add(self, s: &str) -> String,由于第一个参数是self,所以s1的所有权会发生转移。
  • 使用format!宏,比如let s3 = format!("{}{}", s1, s2);,其用法和println!类似,区别在于后者将字符串显示在屏幕上,而前者返回字符串。
读取字符串中的元素

Rust中不能通过索引来获取字符串中的单个字符,比如下面的代码就会报错,

fn main() {
    let s = String::from("hello");
    println!("{}", s[0]);
}

// 报错信息
// 3 |     println!("{}", s[0]);
//   |                    ^^^^ `String` cannot be indexed by `{integer}`
//   |
//   = help: the trait `Index<{integer}>` is not implemented for `String`

其中原因要从Rust对字符串的表示说起。Rust中的字符串是UTF-8编码的,这是一个变长编码,不同字符的编码长度不一样,比如字符a需要一个字节进行编码,而字符你需要三个字节。 Rust中的String实质上是对Vec的封装,Vec存储了字符串的UTF-8编码数据,因此,当执行索引 *** 作[]时,实际上获取的是字符串编码后的某个字节,而不是获取某个字符,所以,为了避免潜在的问题(因为[] *** 作符和预想的不一样),Rust禁止直接对字符串进行索引。

字符串的len方法返回的也是存储字符串所需的字节数量,如下:

fn main() {
    let s = String::from("你好");
    println!("{}", s.len());
}

// 输出
// 6

如果需要访问字符串中的字符,可以调用chars方法,chars方法返回是一个迭代器,通过迭代器可以依次访问字符串中的字符,如果需要依次访问字节,可以使用bytes方法,如下:

fn main() {
    let s = String::from("你好");
    for c in s.chars() {
        print!("{}", c);
    }
    println!("");

    for b in s.bytes() {
        print!("{}, ", b);
    }
    println!("");
}

// 输出
// 你好
// 228, 189, 160, 229, 165, 189,
字符串切片

获取字符串的子串,可以利用字符串切片,但需要注意的是,&x[start..end]中的起始和结束位置,是指字节而不是字符,如果获取的子串不能被正确解码,就会在运行时panic,如下:

fn main() {
    let s = String::from("你好");
    println!("{}", &s[0..1]);
}

// 报错信息
// thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside '你' (bytes 0..3) of `你好`'
哈希表HashMap

哈希表HashMap存放的是键值对,在Rust中,哈希表中所有键的类型必须相同,所有值的类型也必须相同。

创建哈希表

常见的方法是使用new创建一个空哈希表,然后插入元素,如下:

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(String::from("hello"), 1);
    map.insert(String::from("world"), 2);
}

也可以使用迭代器的collect方法创建哈希表,但要求迭代器中的每个元素是元组,元组的第一个值作为键,第二个值作为键对应的值,如下:

use std::collections::HashMap;

fn main() {
    let strings = vec![String::from("hello"), String::from("world")];
    let numbers = vec![1, 2];
    let mut map: HashMap<_, _> = strings.into_iter().zip(numbers.into_iter()).collect();
}

这里需要指定变量的类型是HashMap<_, _>,因为collect方法可以将迭代器转换为不同的类型,比如这里是将迭代器转换为哈希表,但也可以转换成向量,如下:

fn main() {
    let strings = vec![String::from("hello"), String::from("world")];
    let numbers = vec![1, 2];
    let vec: Vec<_> = strings.into_iter().zip(numbers.into_iter()).collect();
    println!("{:?}", vec);
}

// 输出
// [("hello", 1), ("world", 2)]

因此必须显式地表明collect转换后的类型,这里不用表明键和值的类型,因为编译器可以自动推断。

哈希表和所有权

将没有实现Copy特性的对象,比如字符串,插入到哈希表中(作为键或者值),所有权会发生转移,如下:

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    let x = String::from("hello");
    map.insert(x, 1);
    println!("{}", x);
}

// 报错
// 5 |     let x = String::from("hello");
//   |         - move occurs because `x` has type `String`, which does not implement the `Copy` trait
// 6 |     map.insert(x, 1);
//   |                - value moved here
// 7 |     println!("{}", x);
//   |                    ^ value borrowed here after move
访问元素

同向量类似,哈希表也使用get方法访问元素,返回的是Option<&V>类型,如果查询的键不存在,返回None,否则返回Some,注意这里返回的是引用,所以不获取所有权,而且这里是不可变引用,也不能对其解引用后修改,例子如下:

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(String::from("hello"), 1);
    map.insert(String::from("world"), 2);

    let key = String::from("hello");
    let value = map.get(&key);
    match value {
        Some(v) => *v += 1,
        None => println!("None"), 
    }
}

// 报错信息
// 11 |         Some(v) => *v += 1,
//    |              -     ^^^^^^^ `v` is a `&` reference, so the data it refers to cannot be written
//    |              |
//    |              help: consider changing this to be a mutable reference: `&mut i32`

此外,哈希表的get方法,传递的参数类型为&T,这里因为哈希表的键可能是字符串这种没有实现Copy特性的类型,而我们不希望在传参时发生所有权的转移。

类似于向量,哈希表也可以通过迭代器访问,比如for (k, v) in &hash_map {}。

更新哈希表

可以直接调用insert方法向哈希表中插入元素,问题在于如果键存在,则会覆盖原来的键值。

考虑场景:当需要插入的键已经存在于哈希表中,则不插入,否则插入。
这种情况可以使用哈希表的entry方法来处理,将想要插入的键作为参数传递给entry,entry会返回Entry枚举类型,该枚举类型有or_insert方法,将想要插入的值传递给or_insert方法,如果键存在于哈希表中,就不会插入值,否则插入,代码如下:

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(String::from("hello"), 1);
    map.insert(String::from("world"), 2);
    
    map.entry(String::from("hello")).or_insert(3);
    println!("{:?}", map);

    map.entry(String::from("worl")).or_insert(4);
    println!("{:?}", map);
}

// 输出
// {"world": 2, "hello": 1}
// {"worl": 4, "world": 2, "hello": 1}

值得注意的是,entry和or_insert方法的参数不是引用类型,因此会发生所有权的转移。

考虑场景:对哈希表中某键对应的值进行更新,比如计数器。
这种情况也可以使用or_insert方法来处理,or_insert返回的是值的可变引用,因此可以解引用后再进行修改,如下:

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(String::from("hello"), 1);
    map.insert(String::from("world"), 2);

    let v = map.entry(String::from("hello")).or_insert(3);
    *v += 100;
    println!("{:?}", map);
}

// 输出
// {"world": 2, "hello": 101}

这里or_insert方法之所以返回&mut T,而不是Option<&mut T>,是因为当键对应的值不存在时,or_insert方法首先会插入值,再返回可变引用,当键对应的值存在时,直接返回可变引用,所以,or_insert始终可以返回可变引用,就没有必要使用Option枚举类型。

哈希函数

Rust默认使用是哈希函数是SipHash,也可以设置使用其它的哈希函数。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/3991622.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-10-21
下一篇 2022-10-21

发表评论

登录后才能评论

评论列表(0条)

保存