Skip to content

常见集合

Vec

新建 vector

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

更新 vector

    let mut v = Vec::new();

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

如果想要能够改变它的值,必须使用 mut 关键字使其可变。

读取 vector 的元素

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

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

    let third: Option<&i32> = v.get(2);
    match third {
        Some(third) => println!("The third element is {third}"),
        None => println!("There is no third element."),
    }
使用 &[] 会得到一个索引位置元素的引用。当使用索引作为参数调用 get 方法时,会得到一个可以用于 matchOption<&T>

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

    let first = &v[0]; // 引用

    v.push(6); // 错误!

    println!("The first element is: {first}");
在 vector 的结尾增加新元素时,在没有足够空间将所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。

遍历 vector

    let v = vec![100, 32, 57];
    for i in &v {
        println!("{i}");
    }

    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }
为了修改可变引用所指向的值,在使用 += 运算符之前必须使用解引用运算符(*)获取 i 中的值。

因为借用检查器的规则,无论可变还是不可变地遍历一个 vector 都是安全的。 但是如果想在for循环时插入元素,则会产生错误

使用枚举来存储多种类型

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

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

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

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

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

        // do stuff with v
    } // <- v goes out of scope and is freed here

String

Rust 的核心语言中只有一种字符串类型:字符串 slice str,它通常以被借用的形式出现,&str。由于字符串字面值被储存在程序的二进制输出中,因此字符串字面值也是字符串 slices。

新建字符串

很多 Vec 可用的操作在 String 中同样可用,事实上 String 被实现为一个带有一些额外保证、限制和功能的字节 vector 的封装。其中一个同样作用于 Vec<T>String 函数的例子是用来新建一个实例的 new 函数

    let mut s = String::new();
    let data = "initial contents";

    let s = data.to_string();

    // 该方法也可直接用于字符串字面值:
    let s = "initial contents".to_string();
    let s = String::from("initial contents"); // 等同于to_string

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

    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");

更新字符串

使用push_strpush附加字符串

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

    let mut s = String::from("foo");
    s.push_str("bar");

执行这两行代码之后,s 将会包含 foobarpush_str 方法采用字符串 slice,因为我们并不需要获取参数的所有权。例如,示例中我们希望在将 s2 的内容附加到 s1 之后还能使用它。

    let mut s1 = String::from("foo");
    let s2 = "bar";
    s1.push_str(s2);
    println!("s2 is {s2}");

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

push 方法被定义为获取一个单独的字符作为参数,并附加到 String 中。示例展示了使用 push 方法将字母 "l" 加入 String 的代码。

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

执行这些代码之后,s 将会包含 “lol”。

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

    let s1 = String::from("Hello, ");
    let s2 = String::from("world!");
    let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用

字符串 s3 将会包含 Hello, world!

s1 在相加后不再有效的原因,和使用 s2 的引用的原因,与使用 + 运算符时调用的函数签名有关。+ 运算符使用了 add 函数,这个函数签名看起来像这样:

fn add(self, s: &str) -> String {
这个语句会获取 s1 的所有权,附加上从 s2 中拷贝的内容,并返回结果的所有权。

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

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

这些代码也会将 s 设置为 “tic-tac-toe”。format!println! 的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果内容的 String。这个版本就好理解的多,宏 format! 生成的代码使用引用所以不会获取任何参数的所有权。

索引字符串(并不支持)

一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。

    let s1 = String::from("hello");
    let h = s1[0];
错误
3 |     let h = s1[0];
  |             ^^^^^ `String` cannot be indexed by `{integer}`

Rust 的字符串不支持索引。因为rust的字符串是由utf8编码的

字符串 slice

let hello = "Здравствуйте";

let s = &hello[0..4];

当使用&hello[0..1]时,会在运行时panic

因为有效的 Unicode 标量值可能会由不止一个字节组成。

中文3个,英文1个

所以尽量避免使用

遍历字符串

for c in "Зд".chars() {
    println!("{c}");
}

output

З
д

for b in "Зд".bytes() {
    println!("{b}");
}

output

208
151
208
180

`

Hash Map

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

哈希 map

    use std::collections::HashMap; // 导入库

    let mut scores = HashMap::new();

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

注意必须首先 use 标准库中集合部分的 HashMap

访问哈希 map中的值

可以通过 get 方法并提供对应的键来从哈希 map 中获取值,如示例所示:

    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).copied().unwrap_or(0);

get 方法返回 Option<&V>,如果某个键在哈希 map 中没有对应的值,get 会返回 None。程序中通过调用 copied 方法来获取一个 Option<i32> 而不是 Option<&i32>,接着调用 unwrap_orscore 中没有该键所对应的项时将其设置为零。

遍历哈希 map

    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}");
    }

这会以任意顺序打印出每一个键值对:

Yellow: 50
Blue: 10

哈希 map 和所有权

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

    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 不再有效,

insert 调用将 field_namefield_value 移动到哈希 map 中后,将不能使用这两个绑定。

如果将值的引用插入哈希 map,这些值本身将不会被移动进哈希 map。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的。

更新哈希 map

覆盖一个值

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

    use std::collections::HashMap;

    let mut scores = HashMap::new();

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

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

只在键没有对应值时插入键值对

   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,它获取我们想要检查的键作为参数。entry 函数的返回值是一个枚举,Entry,它代表了可能存在也可能不存在的值。Entryor_insert 方法在键对应的值存在时就返回这个值的可变引用,如果不存在则将参数作为新值插入并返回新值的可变引用。

根据旧值更新一个值

    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);

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

哈希函数

HashMap 默认使用一种叫做 SipHash 的哈希函数,它可以抵御涉及哈希表(hash table)1 的拒绝服务(Denial of Service, DoS)攻击。然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价。

你可以指定一个不同的 hasher 来切换为其它函数。hasher 是一个实现了 BuildHasher trait 的类型。

SipHash - Wikipedia