• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

RustRust学习 第八章常见集合

武飞扬头像
StudyWinter
帮助1

Rust 标准库中包含一系列被称为 集合collections)的非常有用的数据结构。大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就已知,并且还可以随着程序的运行增长或缩小。每种集合都有着不同功能和成本,而根据当前情况选择合适的集合,这是一项始终成长的技能。

三个在 Rust 程序中被广泛使用的集合:

  • vector 允许一个挨着一个地储存一系列数量可变的值
  • 字符串string)是一个字符的集合。之前见过 String 类型,不过在本章将深入了解。
  • 哈希 maphash map)允许我们将值与一个特定的键(key)相关联。这是一个叫做 map 的更通用的数据结构的特定实现。

8.1 vector

第一个类型是 Vec<T>,也被称为 vector。vector 允许我们在一个单独的数据结构中储存多于一个的值,它在内存中彼此相邻地排列所有的值。vector 只能储存相同类型的值。

新建vector

为了创建一个新的空 vector,可以调用 Vec::new 函数

  1.  
    fn main() {
  2.  
    let _v: Vec<i32> = Vec::new();
  3.  
     
  4.  
    }

注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值,Rust 并不知道想要储存什么类型的元素,尖括号中就是想要存储的类型。

在更实际的代码中,一旦插入值 Rust 就可以推断出想要存放的类型,所以很少会需要这些类型注解。更常见的做法是使用初始值来创建一个 Vec,而且为了方便 Rust 提供了 vec! 宏。这个宏会根据我们提供的值来创建一个新的 Vec

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

Rust 可以推断出 v 的类型是 Vec<i32>,因此类型注解就不是必须的。

更新vector

对于新建一个 vector 并向其增加元素,可以使用 push 方法

  1.  
    fn main() {
  2.  
    let mut v = Vec::new();
  3.  
     
  4.  
    v.push(5);
  5.  
    v.push(6);
  6.  
    v.push(7);
  7.  
    v.push(8);
  8.  
    v.push(9);
  9.  
     
  10.  
    }

如果想要能够改变它的值,必须使用 mut 关键字使其可变。放入其中的所有值都是 i32 类型的,而且 Rust 也根据数据做出如此判断,所以不需要 Vec<i32> 注解。

丢弃vector时也会丢弃所有元素

当 vector 被丢弃时,所有其内容也会被丢弃,这意味着这里它包含的整数将被清理。这可能看起来非常直观,不过一旦开始使用 vector 元素的引用,情况就变得有些复杂了。

读取vector的元素

访问 vector 中一个值的两种方式,索引语法或者 get 方法:

  1.  
    fn main() {
  2.  
    let v = vec![1, 2, 3, 4, 5];
  3.  
    let third : &i32 = &v[2]; // 索引
  4.  
    println!("The third element is {}", third);
  5.  
     
  6.  
    // get方法
  7.  
    match v.get(3) {
  8.  
    Some(three) => println!("match The third element is {}", three),
  9.  
    None => println!("There is no third element"),
  10.  
    }
  11.  
    }

这里有两个需要注意的地方。首先,使用索引值 2 来获取第三个元素,索引是从 0 开始的。其次,这两个不同的获取第三个元素的方式分别为:使用 & 和 [] 返回一个引用;或者使用 get 方法以索引作为参数来返回一个 Option<&T>

Rust 有两个引用元素的方法的原因是程序可以选择如何处理当索引值在 vector 中没有对应值的情况。作为一个例子,让我们看看如果有一个有五个元素的 vector 接着尝试访问索引为 100 的元素时程序会如何处理。

  1.  
    fn main() {
  2.  
    let v = vec![1, 2, 3, 4, 5];
  3.  
     
  4.  
    let does_not_exist = &v[100];
  5.  
    let does_not_exist = v.get(100);
  6.  
    }

运行

学新通

 当运行这段代码,你会发现对于第一个 [] 方法,当引用一个不存在的元素时 Rust 会造成 panic。这个方法更适合当程序认为尝试访问超过 vector 结尾的元素是一个严重错误的情况,这时应该使程序崩溃。

当 get 方法被传递了一个数组外的索引时,它不会 panic 而是返回 None。当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。接着你的代码可以有处理 Some(&element) 或 None 的逻辑。

一旦程序获取了一个有效的引用,借用检查器将会执行所有权和借用规则来确保 vector 内容的这个引用和任何其他引用保持有效。

在拥有 vector 中项的引用的同时向其增加一个元素

  1.  
    fn main() {
  2.  
    let v = vec![1, 2, 3, 4, 5];
  3.  
     
  4.  
    let first = v[0]; // 不可变引用
  5.  
    v.push(6);
  6.  
    println!("the first element is: {}", first);
  7.  
    }

结果

学新通

不能这么做的原因是由于 vector 的工作方式:在 vector 的结尾增加新元素时,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。

还有些复杂

遍历vector中的元素

使用 for 循环来获取 i32 值的 vector 中的每一个元素的不可变引用并将其打印:

  1.  
    fn main() {
  2.  
    let v = vec![1, 2, 3, 4, 5];
  3.  
     
  4.  
    for i in &v {
  5.  
    println!("{}", i);
  6.  
    }
  7.  
    }

也可以遍历可变 vector 的每一个元素的可变引用以便能改变他们

  1.  
    fn main() {
  2.  
    let mut v = vec![1, 2, 3, 4, 5];
  3.  
     
  4.  
    for i in &mut v {
  5.  
    *i = 50;
  6.  
    }
  7.  
    }

使用枚举来储存多种类型

枚举的成员都被定义为相同的枚举类型,所以当需要在 vector 中储存不同类型值时,可以定义并使用一个枚举!

假如我们想要从电子表格的一行中获取值,而这一行的有些列包含数字,有些包含浮点值,还有些是字符串。我们可以定义一个枚举,其成员会存放这些不同类型的值,同时所有这些枚举成员都会被当作相同类型,那个枚举的类型。接着可以创建一个储存枚举值的 vector,这样最终就能够储存不同类型的值了。

  1.  
    #[derive(Debug)]
  2.  
    enum SpreadsheetCell {
  3.  
    Int(i32),
  4.  
    Float(f64),
  5.  
    Text(String),
  6.  
    }
  7.  
     
  8.  
     
  9.  
    fn main() {
  10.  
     
  11.  
     
  12.  
    let row = vec![
  13.  
    SpreadsheetCell::Int(3),
  14.  
    SpreadsheetCell::Float(10.12),
  15.  
    SpreadsheetCell::Text(String::from("blue")),
  16.  
    ];
  17.  
     
  18.  
    // 打印
  19.  
    for item in row.iter() {
  20.  
    println!("Enum item: {:?}", item);
  21.  
    }
  22.  
    }
学新通

Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存。第二个好处是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。

vector除了 push 之外还有一个 pop 方法,它会移除并返回 vector 的最后一个元素。

8.2 字符串

在集合章节中讨论字符串的原因是,字符串就是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。

什么是字符串

在开始深入这些方面之前,我们需要讨论一下术语 字符串 的具体意义。Rust 的核心语言中只有一种字符串类型:str,字符串 slice,它通常以被借用的形式出现&str。第四章讲到了 字符串 slice:它们是一些储存在别处的 UTF-8 编码字符串数据的引用。比如字符串字面值被储存在程序的二进制输出中,字符串 slice 也是如此。

称作 String 的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。当 Rustacean 们谈到 Rust 的 “字符串”时,它们通常指的是 String 和字符串 slice &str 类型,而不仅仅是其中之一。虽然本部分内容大多是关于 String 的,不过这两个类型在 Rust 标准库中都被广泛使用,String 和字符串 slice 都是 UTF-8 编码的。

新建字符串

很多 Vec 可用的操作在 String 中同样可用,从以 new 函数创建字符串开始

  1.  
    fn main() {
  2.  
    let mut s = String::new();
  3.  
    }

这新建了一个叫做 s 的空的字符串,接着我们可以向其中装载数据。通常字符串会有初始数据,因为我们希望一开始就有这个字符串。为此,可以使用 to_string 方法,它能用于任何实现了 Display trait 的类型,字符串字面值也实现了它。

  1.  
    fn main() {
  2.  
    let data = "hello world";
  3.  
    let s = data.to_string();
  4.  
     
  5.  
     
  6.  
    // 也可以直接用于字符串字面值
  7.  
    let s = "hello world".to_string();
  8.  
    }

也可以使用 String::from 函数来从字符串字面值创建 String

  1.  
    fn main() {
  2.  
     
  3.  
    let s = String::from("value");
  4.  
     
  5.  
    }

字符串是 UTF-8 编码的,所以可以包含任何可以正确编码的数据

  1.  
    fn main() {
  2.  
    let hello = String::from("السلام عليكم");
  3.  
    let hello = String::from("Dobrý den");
  4.  
    let hello = String::from("Hello");
  5.  
    let hello = String::from("שָׁלוֹם");
  6.  
    let hello = String::from("नमस्ते");
  7.  
    let hello = String::from("こんにちは");
  8.  
    let hello = String::from("안녕하세요");
  9.  
    let hello = String::from("你好");
  10.  
    let hello = String::from("Olá");
  11.  
    let hello = String::from("Здравствуйте");
  12.  
    let hello = String::from("Hola");
  13.  
    }

更新字符串

String 的大小可以增加,其内容也可以改变,就像可以放入更多数据来改变 Vec 的内容一样。另外,可以方便的使用   运算符或 format! 宏来拼接 String 值。

使用push_str和push附加字符串

可以通过 push_str 方法来附加字符串 slice,从而使 String 变长

  1.  
    fn main() {
  2.  
    let mut hello = String::from("foo");
  3.  
    hello.push_str("bar");
  4.  
    println!("{}", hello);
  5.  
     
  6.  
    }

执行这两行代码之后,s 将会包含 foobarpush_str 方法采用字符串 slice,因为我们并不需要获取参数的所有权。

如果将 s2 的内容附加到 s1 之后,自身不能被使用就糟糕了。

  1.  
    fn main() {
  2.  
    let mut s1 = String::from("hello");
  3.  
    let s2 = "world";
  4.  
    s1.push_str(s2);
  5.  
    println!("s2 is {}", s2);
  6.  
     
  7.  
    }

如果 push_str 方法获取了 s2 的所有权,就不能在最后一行打印出其值了。好在代码如我们期望那样工作!

push 方法被定义为获取一个单独的字符作为参数,并附加到 String 中。

  1.  
    fn main() {
  2.  
    let mut s1 = String::from("hello");
  3.  
    s1.push('L');
  4.  
     
  5.  
    }

使用 运算符或format!宏拼接字符串

通常你会希望将两个已知的字符串合并在一起。一种办法是像这样使用   运算符

  1.  
    fn main() {
  2.  
    let s1 = String::from("hello");
  3.  
    let s2 = String::from("world");
  4.  
    let s3 = s1 &s2;
  5.  
    // s1被移动了,不能使用
  6.  
    }

字符串 s3 将会包含 Helloworlds1 在相加后不再有效的原因,和使用 s2 的引用的原因,与使用   运算符时调用的函数签名有关。  运算符使用了 add 函数,这个函数签名看起来像这样:

fn add(self, s: &str) -> String {

首先,s2 使用了 &,意味着我们使用第二个字符串的 引用 与第一个字符串相加。这是因为 add 函数的 s 参数:只能将 &str 和 String 相加,不能将两个 String 值相加。不过等一下 —— 正如 add 的第二个参数所指定的,&s2 的类型是 &String 而不是 &str。那么为什么示例还能编译呢?

之所以能够在 add 调用中使用 &s2 是因为 &String 可以被 强转(coerced)成 &str。当add函数被调用时,Rust 使用了一个被称为 解引用强制多态(deref coercion)的技术,你可以将其理解为它把 &s2 变成了 &s2[..]

其次,可以发现签名中 add 获取了 self 的所有权,因为 self 没有 使用 &。这意味着示例中的 s1 的所有权将被移动到 add 调用中,之后就不再有效。所以虽然 let s3 = s1 &s2; 看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取 s1 的所有权,附加上从 s2 中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝,不过实际上并没有:这个实现比拷贝要更高效。

索引字符串

在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。然而在 Rust 中,如果你尝试使用索引语法访问 String 的一部分,会出现一个错误。

  1.  
    fn main() {
  2.  
    let s1 = String::from("hello");
  3.  
    let h = s1[0];
  4.  
    }

结果

学新通

错误和提示说明了全部问题:Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?为了回答这个问题,我们必须先聊一聊 Rust 是如何在内存中储存字符串的。

内部表现

String 是一个 Vec<u8> 的封装。

  1.  
    fn main() {
  2.  
    let len1 = String::from("Hola").len();
  3.  
     
  4.  
    let len2 = String::from("Здравствуйте").len();
  5.  
    println!("{}, {}", len1, len2);
  6.  
     
  7.  
    }

在这里,len 的值是 4 ,这意味着储存字符串 “Hola” 的 Vec 的长度是四个字节:这里每一个字母的 UTF-8 编码都占用一个字节。当问及Здравствуйте这个字符是多长的时候有人可能会说是 12。然而,Rust 的回答是 24。这是使用 UTF-8 编码 “Здравствуйте” 所需要的字节数,这是因为每个 Unicode 标量值需要两个字节存储。

因为编码的原因,字符串使用索引时要特别谨慎。

遍历字符串的方法

如果你需要操作单独的 Unicode 标量值,最好的选择是使用 chars 方法。对 “नमस्ते” 调用 chars 方法会将其分开并返回六个 char 类型的值,接着就可以遍历其结果来访问每一个元素了:

  1.  
    fn main() {
  2.  
    for c in "नमस्ते".chars() {
  3.  
    println!("{}", c);
  4.  
    }
  5.  
    }

结果

学新通

bytes 方法返回每一个原始字节,这可能会适合你的使用场景:

  1.  
    fn main() {
  2.  
    for c in "नमस्ते".bytes() {
  3.  
    println!("{}", c);
  4.  
    }
  5.  
    }

 结果

学新通

总结

总而言之,字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理 String 数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何预先处理 UTF-8 数据。这种权衡取舍相比其他语言更多的暴露出了字符串的复杂性,不过也使你在开发生命周期后期免于处理涉及非 ASCII 字符的错误。

8.3 哈希map

HashMap<K, V> 类型储存了一个键类型 K 对应一个值类型 V 的映射。它通过一个 哈希函数hashing function)来实现映射,决定如何将键和值放入内存中。

新建一个哈希map

可以使用 new 创建一个空的 HashMap,并使用 insert 增加元素。

  1.  
    use std::collections::HashMap;
  2.  
    fn main() {
  3.  
    let mut scores = HashMap::new();
  4.  
    // 使用insert
  5.  
    scores.insert(String::from("Blue"), 10);
  6.  
    scores.insert(String::from("Green"), 50);
  7.  
     
  8.  
    }

注意必须首先 use 标准库中集合部分的 HashMap。在这三个常用集合中,HashMap 是最不常用的,所以并没有被 prelude 自动引用。

像 vector 一样,哈希 map 将它们的数据储存在堆上,这个 HashMap 的键类型是 String 而值类型是 i32。类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。

另一个构建哈希 map 的方法是使用一个元组的 vector 的 collect 方法,其中每个元组包含一个键值对。

  1.  
    use std::collections::HashMap;
  2.  
    fn main() {
  3.  
    let teams = vec![String::from("Blue"), String::from("Green")];
  4.  
    let data = vec![10, 50];
  5.  
    // 复杂
  6.  
    let scores : HashMap<_, _> = teams.iter().zip(data.iter()).collect();
  7.  
     
  8.  
    for i in &scores {
  9.  
    println!("{}, {}", i.0, i.1);
  10.  
    }
  11.  
     
  12.  
    }

这里 HashMap<_, _> 类型注解是必要的,因为可能 collect 很多不同的数据结构,而除非显式指定否则 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 HashMap 所包含的类型。

哈希map和所有权

对于像 i32 这样的实现了 Copy trait 的类型,其值可以拷贝进哈希 map。对于像 String 这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者。

  1.  
    use std::collections::HashMap;
  2.  
    fn main() {
  3.  
     
  4.  
    let field_name = String::from("Favorite color");
  5.  
    let field_value = String::from("Blue");
  6.  
     
  7.  
    let mut map = HashMap::new();
  8.  
    map.insert(field_name, field_value);
  9.  
    // 这里 field_name 和 field_value 不再有效,
  10.  
     
  11.  
    println!("{}, {}", field_name, field_value);
  12.  
    }

结果

学新通

访问哈希map中的值

可以通过 get 方法并提供对应的键来从哈希 map 中获取值

  1.  
    use std::collections::HashMap;
  2.  
    fn main() {
  3.  
    let mut scores = HashMap::new();
  4.  
     
  5.  
    scores.insert(String::from("Blue"), 10);
  6.  
    scores.insert(String::from("Yellow"), 50);
  7.  
     
  8.  
    let team_name = String::from("Blue");
  9.  
    //使用get
  10.  
    let score = scores.get(&team_name);
  11.  
    println!("{:?}", score);
  12.  
    }

可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是 for 循环:

  1.  
    use std::collections::HashMap;
  2.  
    fn main() {
  3.  
    let mut scores = HashMap::new();
  4.  
     
  5.  
    scores.insert(String::from("Blue"), 10);
  6.  
    scores.insert(String::from("Yellow"), 50);
  7.  
     
  8.  
    for (k, v) in &scores {
  9.  
    println!("{}, {}", k, v)
  10.  
    }
  11.  
    }

这样也可以

  1.  
    use std::collections::HashMap;
  2.  
    fn main() {
  3.  
    let mut scores = HashMap::new();
  4.  
     
  5.  
    scores.insert(String::from("Blue"), 10);
  6.  
    scores.insert(String::from("Yellow"), 50);
  7.  
     
  8.  
    for item in &scores {
  9.  
    println!("{}, {}", item.0, item.1)
  10.  
    }
  11.  
    }

更新哈希map

尽管键值对的数量是可以增长的,不过任何时候,每个键只能关联一个值。当我们想要改变哈希 map 中的数据时,必须决定如何处理一个键已经有值了的情况。可以选择完全无视旧值并用新值代替旧值。可以选择保留旧值而忽略新值,并只在键 没有 对应值时增加新值。或者可以结合新旧两值。

覆盖一个值

如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。

  1.  
    use std::collections::HashMap;
  2.  
    fn main() {
  3.  
    let mut scores = HashMap::new();
  4.  
     
  5.  
    scores.insert(String::from("Blue"), 10);
  6.  
    scores.insert(String::from("Blue"), 50); // 后面出現的覆蓋前面出現的
  7.  
    println!("{:?}", scores);
  8.  
    }

只在键没有对应值时插入

经常会检查某个特定的键是否有值,如果没有就插入一个值。

  1.  
    use std::collections::HashMap;
  2.  
    fn main() {
  3.  
    let mut scores = HashMap::new();
  4.  
     
  5.  
    scores.insert(String::from("Green"), 10);
  6.  
    scores.insert(String::from("Blue"), 50); // 后面出現的覆蓋前面出現的
  7.  
     
  8.  
    scores.entry(String::from("Green")).or_insert(90); // Green不存在就插入,存在就不插入
  9.  
     
  10.  
    println!("{:?}", scores);
  11.  
    }

Entry 的 or_insert 方法在键对应的值存在时就返回这个值的 Entry,如果不存在则将参数作为新值插入并返回修改过的 Entry

根据旧值更新一个值

另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它。

  1.  
    use std::collections::HashMap;
  2.  
    fn main() {
  3.  
    // 統計單詞出現的次數
  4.  
    let text = "hello world wonderful world";
  5.  
    let mut map = HashMap::new();
  6.  
     
  7.  
    for word in text.split_ascii_whitespace() {
  8.  
    let count = map.entry(word).or_insert(0);
  9.  
    *count = 1;
  10.  
    }
  11.  
    println!("{:?}", map);
  12.  
    }

这会打印出 {"world": 2, "hello": 1, "wonderful": 1}or_insert 方法事实上会返回这个键的值的一个可变引用(&mut V)。这里我们将这个可变引用储存在 count 变量中,所以为了赋值必须首先使用星号(*)解引用 count。这个可变引用在 for 循环的结尾离开作用域,这样所有这些改变都是安全的并符合借用规则。

哈希函数

HashMap 默认使用一种 “密码学安全的”(“cryptographically strong” )1 哈希函数,它可以抵抗拒绝服务(Denial of Service, DoS)攻击。然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 hasher 来切换为其它函数。hasher 是一个实现了 BuildHasher trait 的类型。

参考:常见集合 - Rust 程序设计语言 简体中文版 (bootcss.com)

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgkhifj
系列文章
更多 icon
同类精品
更多 icon
继续加载