09 常见集合类型

09 常见集合类型

------正文内容展示,开始汲取新知识啦------

Rust 标准库中包含一系列被称为 集合(collections)的非常有用的数据结构。大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就已知,并且还可以随着程序的运行增长或缩小。以下三个是在 Rust 程序中被广泛使用的集合:

  • 动态数组/向量(vector):可以连续地存储任意多个值
  • 字符串(string):是字符的集合。
  • 哈希映射(hash map):允许我们将值与一个特定的键(key)相关联。这是数据结构 映射(map)的特定实现。

对于标准库提供的其他类型的集合,请查看文档

使用动态数组(Vector)存储多个值

Vector允许我们在同一个数据结构中存储多个相同类型的值,并且这些在内存中相邻排列。

创建 vector

调用Vec::new函数,创建一个Vector:

let v : Vec<i32> = Vec::new();

新建一个包含1,2,3的Vector:

let v = vec![1,2,3];

更新 vector

let mut v  = Vec::new();

v.push(5);
v.push(6);
v.push(7);
v.push(8);

销毁 vector 时也会销毁其中的元素

和其他的 struct 一样,vector一旦离开作用域就会被立即销毁:

fn main() {
    {
        let v = vec![1, 2, 3, 4];
        //v 的相关操作
    } // <- 这里 v 离开作用域并被丢弃
}

当 vector 被销毁时,其中元素也会被销毁。也就意味着一旦 vector 被销毁,vector 中元素的引用也会全部失效。

读取 vector 中的元素

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    //1、使用索引
    let third: &i32 = &v[2];
    println!("The third element is {}", third);

    //2.get()方法
    match v.get(2) {
        Some(third) => println!("The third element is {}", third),
        None => println!("There is no third element."),
    }
}

Rust 提供了两种引用元素的方法的原因是当尝试使用现有元素范围之外的索引值时可以选择让程序如何运行。

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    //1.索引
    let does_not_exist = &v[100];
    //2.get
    let does_not_exist = v.get(100);
}

当运行这段代码,使用索引时,当引用一个不存在的元素时 Rust 会造成 panic。如果我们希望在越界访问时让程序崩溃,就用这个方法。

get 方法会在检测到索引越界时返回None,而不是让程序直接崩溃,因此当偶尔越界访问是不可避免时,应该使用这个方法。

我们知道,一旦程序获得一个有效引用,借用检查器就会执行所有权规则和借用规则,来确保这个引用和其他任何指向这个值的引用始终有效,并且我们不能在同一个作用域拥有这个值的可变引用和不可变引用。使用 vector 也遵循这个约定,当我们持有了 vector 的首个元素的不可变引用,此时再向 vector 的末尾添加元素,结果是失败的。

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    let first = &v[0];

    v.push(6);

    println!("The first element is: {}", first);
}
09 常见集合类型插图

为什么对第一个元素的引用需要关心 vector 末尾处的变化?这是 vector 的工作原理导致的:vector 中元素是连续存储的,插入新的元素后也许没有足够多的空间将所有元素依次放下,此时需要重新分配内存空间,并将新的元素移动到新的空间上。这个例子中,第一个元素的引用可能会因为插入行为而指向被释放的内存。

遍历 vector 中的元素

使用 for 循环遍历 vector:

let v = vec![100,32,75];
for i in &v {
    println!("{}",i)
}
let mut v = vec![100,32,75];
for i in &mut v {
    //需要使用解引用获得i绑定的值
    *i += 1;
}

使用枚举存储多个类型的值

vector 只能存放相同类型的值,而实际情况总会遇到需要存储不同类型的值,这时候我们就会想到用枚举。

假设我们希望读取表格中的单元值,这些单元值可能是整数、浮点数和字符串,那么就可以使用枚举的不同变体来存放不同类型的值。但是这些枚举变体都会被视作统一的类型:也就是这个枚举类型。接着,我们就可以创建一个持有该枚举类型的vector,来存取类型不同的值:

fn main() {
    enum SpreadsheetCell {
        Int(i32),
        Float(f64),
        Text(String),
    }

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

为了计算出元素在堆上使用的存储空间,Rust 需要在编译时确定 vector 中元素的类型。使用枚举的另一个好处在于它可以显示的列举处所有 vector 值类型。假如 Rust 允许 vector 存储任意类型那么在对 vector 中的元素进行操作时就有可能会因为一个或多个不当的类型处理而导致错误。将枚举和match表达式搭配使用,意味着 Rust 可以在编译时确保所有可能的情形都可以得到妥当的处理。

如果在编写程序时不能确切无遗地知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,可以使用 trait 对象。

这是一些使用 vector 的最常见的方式,在标准库中还有很多 Vec 定义的其他实用方法的 API 文档

使用字符串存储UTF-8编码的文本

什么是字符串?

Rust 的核心语言中只有一种字符串类型:字符串切片str,它通常以被借用的形式出现,&str。字符串切片是一些指向存储在别处的UTF-8编码字符串的引用。

当我们谈及字符串时,一般是指 String 和 字符串切片 &str 两种类型,而不仅仅只是其中一种,这两种类型广泛地用于 Rust 标准库中,并且都采用了 UTF-8 编码。

创建字符串

和 vector 一样,采用 new函数创建字符串:

let mut s = String::new();

这是一个空的字符串,我们向其中填入数据,但是一般而言,字符串都会有初始数据,对于这种情况,可以使用 to_string 方法,它能用于任何实现了 Display trait 的类型,字符串字面量也是如此:

fn main() {
    let data = "initial contents";

    let s = data.to_string();

    // 该方法也可直接用于字符串字面量:
    let s = "initial contents".to_string();
}

也可以使用 String::from 函数来从字符串字面值创建 String。代码等同于使用 to_string:

let s = String::from("initial contents");

由于字符串是基于 UTF-8 编码的,下方的字符串数据都是合法的:

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

更新字符串

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

使用push_str和push向字符串中添加内容

使用 push_str 方法向 String 中添加一段字符串切片:

fn main() {
    let mut s = String::from("foo");
    s.push_str("bar");
}

push_str 方法并没有转移 s 的所有权,而是接收了 s 的一个可变引用,所以在执行完这两行代码后 s 仍然可以被使用。

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

fn main() {
    let mut s = String::from("lo");
    s.push('l');
}

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

fn main() {
    let s1 = String::from("Hello, ");
    let s2 = String::from("world!");
    //使用运算符 + 拼接字符串
    let s3 = s1 + &s2; //s1 被移动了,不能继续使用
}

s1 在相加后不再有效的原因,和使用 s2 的引用的原因,与使用 + 运算符时调用的函数签名有关。+ 运算符使用了 add 函数:fn add(self, s: &str) -> String {},这并不是标准库中实际的签名;标准库中的 add 使用泛型定义。这里我们看到的 add 的签名使用具体类型代替了泛型,这也正是当使用 String 值调用这个方法会发生的。

此外,s2 使用了 &,意味着我们使用第二个字符串的 引用 与第一个字符串相加。这是因为 add 函数的 s 参数:只能将 &str 和 String 相加,不能将两个 String 值相加。之所以能够在 add 调用中使用 &s2 是因为 &String 可以被 强转(coerced)成 &str。当add函数被调用时,Rust 使用了 Deref 强制转换(deref coercion),可以将其理解为它把 &s2 变成了&s2[..]。同时 add 没有获取参数的所有权,所以 s2 在这个操作后仍然是有效的 String。但是,add 获取了 self 的所有权,即意味着 s1 的所有权被移动了,后续不再有效。换句话讲,add 获取原来变量内容的所有权和拷贝参数值内容并将结果的所有权返回赋给新的变量。

如果想拼接多个字符串,+就会显得笨重,这时我们可以使用 format! 宏:

fn main() {
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = format!("{}-{}-{}", s1, s2, s3);
}

format!println! 的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果内容的 String。同时,format! 也不会获取参数的所有权。

字符串索引

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

fn main() {
    let s1 = String::from("hello");
    let h = s1[0];
}
09 常见集合类型插图1

这个错误告诉我们:Rust 中字符串不支持索引。

字符串之所以不支持索引的原因,是因为它的存储方式。String 实际上是一个基于Vec<u8>的封装类型,如:

  let len = String::from("Hola").len();

在这里,len 的值是 4 ,这意味着储存字符串 “Hola” 的 Vec 的长度是4个字节:这里每一个字母的 UTF-8 编码都占用一个字节。

let len = String::from("Здравствуйте").len();//这个字符串中的首字母是西里尔字母的 Ze 
                                             //而不是阿拉伯数字 3 

Rust中此字符串的长度是24:这是因为每个 Unicode 标量值需要两个字节存储。因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。例如下方的代码就是无效的,

let hello = "Здравствуйте";
let answer = &hello[0];

已经知道 answer 不是第一个字符 З。当使用 UTF-8 编码时,З 的第一个字节 208,第二个是 151,所以 answer 实际上应该是 208,不过 208 自身并不是一个有效的字母。但它是 Rust 在字节索引 0 位置所能提供的唯一数据。用户通常不会想要一个字节值被返回,即便这个字符串只有拉丁字母: 即便 &"hello"[0] 是返回字节值的有效代码,它也应当返回 104 而不是 h。

至此,已经解释了为什么Rust字符串不支持索引。即字符串中字符对应的标量值可能是多个字节存储的,一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。

这引起了关于 UTF-8 的另外一个问题:从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节标量值字形簇(最接近人们眼中 字母 的概念)。

比如这个用梵文书写的印度语单词 “नमस्ते”,

字节角度最终它储存在 vector 中的 u8 值看起来像这样:

//这里有 18 个字节,也就是计算机最终会储存的数据。
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135]

Unicode 标量值的角度理解它们,也就像 Rust 的 char 类型那样,这些字节看起来像这样:

['न', 'म', 'स', '्', 'त', 'े']
//这里有六个 char,不过第四个和第六个都不是字母,它们是发音符号本身并没有任何意义

如果以字形簇的角度理解,就会得到人们所说的构成这个单词的四个字母:

["न", "म", "स्", "ते"]

最后一个 Rust 不允许使用索引获取 String 字符的原因是,索引操作预期总是需要常数时间 (O(1))。但是对于 String 不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。

字符串切片

上面说到,rust 字符串是不支持索引的,因为我们不知道返回的类型是字节还是字符或者是字形簇,甚至是字符串切片。不过,字符产切片可以通过[]+范围来定义表示,而不是单个数字进行索引。

fn main() {
let hello = "Здравствуйте";

let s = &hello[0..4];
    //s 会是一个 &str,它包含字符串的头四个字节                 
    //因为字符串中每个字母都是两个字节,所以 s 是“Зд”。
    //如果获取 &hello[0..1],Rust 在运行时会 panic,就跟访问 vector 中的无效索引时一样
}

遍历字符串

操作字符串每一部分的最好的方法是明确表示需要字符还是字节。对于单独的 Unicode 标量值使用chars方法。对 “नमस्ते” 调用chars方法会将其分开并返回六个 char 类型的值,以此可以遍历其结果来访问每一个元素:

fn main() {
    for c in "नमस्ते".chars() {
        println!("{}", c);
    }
}
/*
打印结果:
न
म
स
्
त
े
*/

另外 bytes 方法返回每一个原始字节,

fn main() {
    for b in "नमस्ते".bytes() {
        println!("{}", b);
    }
}
/*
打印结果——组成 String 的 18 个字节:
224
164
……
165
135
*/

从字符串中获取字形簇是很复杂的,所以标准库并没有提供这个功能。crates.io 上有些提供这样功能的 crate。

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

使用 HashMap 储存键值对

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

创建 HashMap

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

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);
}

必须首先 use 标准库中集合部分的 HashMap。在这三个常用集合中,HashMap 是最不常用的,所以并没有被 prelude 自动引用。标准库中对 HashMap 的支持也相对较少,例如,并没有内建的构建宏。

另一个构建哈希 map 的方法是在一个元组的 vector 上使用迭代器(iterator)和 collect 方法,其中每个元组包含一个键值对

fn main() {
    use std::collections::HashMap;

    let teams = vec![String::from("Blue"), String::from("Yellow")];
    let initial_scores = vec![10, 50];

    let mut scores: HashMap<_, _> =
        teams.into_iter().zip(initial_scores.into_iter()).collect();
}

collect 方法可以将数据收集进一系列的集合类型,包括 HashMap。例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用 zip 方法来创建一个元组的迭代器,其中 “Blue” 与 10 是一对,依此类推。接着就可以使用 collect 方法将这个元组的迭代器转换成一个 HashMap。

HashMap 中的所有权

对于实现了 Copy trait 的类型,例如 i32,其值会被直接拷贝到 hash map 中。而对于像 String 这种有所有权的类型,其值的所有权会被移动到 hash map 中。

fn main() {
    use std::collections::HashMap;

    let field_name = String::from("Favorite color");
    let field_value = String::from("Blue");

    let mut map = HashMap::new();
    map.insert(field_name, field_value);
    // 这里 field_name 和 field_value 不再有效,若尝试继续使用,将会比编译错误
}

如果将值的引用插入hash map,其值所有权自然不会被移动进hash map。但是这些引用指向的值必须保证在哈hash map 有效时也是有效的。

访问 HashMap 中的值

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

fn main() {
    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 = scores.get(&team_name);
}

这里,score 是蓝队分数,值是 Some(10)。因为 get 返回 Option<V>,所以结果被装进 Some;如果某个键在哈希 map 中没有对应的值,get 会返回 None。

也可以用 for 循环遍历 HashMap 中的所有键值对:

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

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

更新 HashMap

和其他语言一样,插入同 k 不同 v时后者会将前者覆盖(一 一对应)。

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{:?}", scores);//这会打印出 {"Blue": 25}。原始的值 10 则被覆盖了。

我们经常遇到需要检查某个特定的 k 是否有值,如果没有则插入一个新的值。

HashMap 中有个 api 叫做 entry,它获取我们想要检查的键作为参数。entry 函数的返回值是一个枚举,Entry,它代表了可能存在也可能不存在的值。

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);

    scores.entry(String::from("Yellow")).or_insert(50);
    scores.entry(String::from("Blue")).or_insert(50);

    println!("{:?}", scores);
}

Entry 的 or_insert 方法在键对应的值存在时就返回这个值的可变引用,如果不存在则将参数作为新值插入并返回新值的可变引用。

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

fn main() {
    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);
}

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

哈希函数

HashMap 默认使用一种叫做 SipHash 的哈希函数,它可以抵御涉及哈希表(hash table)的拒绝服务(Denial of Service, DoS)攻击。然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,可以指定一个不同的 哈希计算工具(hasher) 来切换为其它函数。hasher 是一个实现了 BuildHasher trait 的类型。

练习

给定一系列数字,使用 vector 并返回这个列表的平均数、中位数和众数。

use std::collections::HashMap;

fn main() {
    let mut vec = vec![1, 2, 3, 8, 9,9,9, 8, 5, 4];
    println!("vector is {:?}", vec);
    let len = vec.len();
    let mut sum = 0;
    for num in &vec {
        sum += num;
    }
    let avg: f32 = sum as f32 / len as f32;
    println!("vector avg is {}", avg);


    array_sort(&mut vec);
    let mid = if len % 2 == 0 {
       (vec[len / 2] + vec[len / 2 - 1]) as f32 / 2 as f32
    } else {
        vec[len / 2] as f32
    };
    println!("vector mid is {}", mid);

    let mut map = HashMap::new();
    for num in &vec {
        let count = map.entry(*num).or_insert(0);
        *count += 1;
    }
    let mode= map.iter().max_by(|a,b|a.1.cmp(&b.1)).unwrap();

    println!("vector mode is {:?}",mode.0);
}


fn array_sort(vec: &mut Vec<i32>) -> &mut Vec<i32> {
    for i in 0..(vec.len() - 1) {
        for j in 0..(vec.len() - 1 - i) {
            if vec[j] > vec[j + 1] {
                let temp = vec[j + 1];
                vec[j + 1] = vec[j];
                vec[j] = temp;
            }
        }
    }
    vec
}

将字符串转换为 Pig Latin,也就是每一个单词的第一个辅音字母被移动到单词的结尾并增加 “ay”,所以 “first” 会变成 “irst-fay”。元音字母(a,e,i,o,u)开头的单词则在结尾增加 “hay”(“apple” 会变成 “apple-hay”)。

fn pig_latin(g: &str) -> String {
    // 转换成 string
    let general = g.to_string();
    let mut is_vowel = false;
    let vowel = vec!['a', 'e', 'i', 'o', 'u'];
    // 获取首字母, 查看是元音还是辅音
    let mut word = ' ';
    if let Some(c) = general.chars().next() {
        word = c;
        for k in &vowel {
            if c == *k {
                is_vowel = true;
                break;
            }
        }
    }
    return if is_vowel {
        // 首字母是元音
        format!("{}-hey", general)
    } else {
        let sub_string = general.replacen(&word.to_string(), "", 1);
        format!("{}-{}ay", sub_string, word)
    };
}

fn main() {
    let t0 = "apple";
    let t1 = "first";
    let t2 = "苹果";
    println!("{} => {}",t0,pig_latin(t0));
    println!("{} => {}",t1,pig_latin(t1));
    println!("{} => {}",t2,pig_latin(t2));
}

使用hash map 和 vector,创建一个文本接口来允许用户向公司的部门中增加员工的名字。例如,“Add Sally to Engineering” 或 “Add Amir to Sales”。接着让用户获取一个部门的所有员工的列表,或者公司每个部门的所有员工按照字典序排列的列表。

use std::{io::stdin, collections::HashMap}; 

fn main() {
    let mut department_map: HashMap<String, Vec<String>> = HashMap::new();
    loop {
        println!("输入Add {{员工}} to {{部门}}");
        let mut input = String::new();
        if let Ok(_len) = stdin().read_line(&mut input) {
            let words: Vec<&str> = input.trim().split_whitespace().collect();
            if !words[0].eq_ignore_ascii_case("add") || !words[2].eq_ignore_ascii_case("to") {
                println!("输入错误,请重新输入!");
                continue;
            }
            let department = String::from(words[3]);
            let employee = String::from(words[1]);
            let hr = department_map.entry(department).or_insert(Vec::new());
            hr.push(employee);
            println!("{:#?}", department_map);
        } else {
            println!("Input error");
        }
    }
}
温馨提示:本文最后更新于2024-05-04 10:57:03,某些文章具有时效性,若有错误或已失效,请在下方留言或QQ联系站长
------正文内容展示,开始汲取新知识啦------

感谢您的访问,Ctrl+D收藏本站吧。

09 常见集合类型 | NS云社区
09 常见集合类型
此内容为免费阅读,请登录后查看
R币0
自助提取
疑问帮助
免费阅读
© 版权声明
THE END
喜欢就支持一下吧
点赞0赞赏 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容