08常见集合

集合(collections)由标准库(std)提供;

集合(collections)指向的数据是存储在上的,可以随着程序的运行增长或缩小。

  • 集合可以存储多个值

使用可变长数组(Vector)存储一系列的值

Vec<T>,也被称为 vector。

  • 可以存储多个值;
  • 只能存储相同类型的值;
  • 在内存中彼此相邻地排列所有值。
    • 内存连续

与slices类似,其大小在编译期是不可知的。

一个 vector 由三部分组成:

  • 指向堆上数据的指针;
  • 当前长度;
  • 当前总容量;
    • 表示vector预留的内存大小。当长度超过阈值,就会扩容。

新建 vector

有两种方法

方法一:使用Vec::new函数创建新的空 vector。

1
2
// 新建一个空的 vector,存储 i32 类型的值;
let v: Vec<i32> = Vec::new();

注意:当新建空的 vector 时,

  1. 如果不存储数据,那么 Rust 无法知道存储什么类型的元素,所以要声明变量类型;
  2. 如果后面添加数据,则可以不用显示声明变量类型;

方法二:使用 vec! 宏,提供初始值来创建一个 vec

1
2
// 新建一个包含初值的 vector;
let v = vec![1,2,3];

因为提供的初始值为 i32 类型的,所以 Rust 可以推断出 v 的类型是 Vec<i32>,因此可以省略标注类型。

插入元素

使用 push 向 vector 中添加其他元素;

1
2
3
let mut v = Vec::new();
v.push(5);
v.push(6);

想改变 v 的值,需要使用 mut 关键字使其可变。

因为放入的值都是 i32 类型的,所以这里也可以省略类型标注。

不能向 immutable vector 中添加数据;

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

与其他 struct 类似,vector 在其离开作用域时会被释放。其中包含的数据也将被清理。

读取 vector 的元素

有两种方法引用 vector 中存储的值。

方法一:索引语法(索引从 0 开始),[<索引>] , 返回一个引用;

方法二:get(<索引>) 方法,返回一个 Option<&T>

1
2
3
4
let v = vec![1,2,3];
let third: &i32 = &v[2]; // 这里 third = 3;

let second: Option<&i32> = v.get(1); // 这里 second = 2;

注意:Rust 提供两种引用元素的方法,我们可以选择如何处理当索引值在 vector 中没有对应值的情况。

  • 索引语法,[] ,当引用一个不存在的元素时,Rust 会发生 panic。
    • 适合当程序认为尝试访问超过 vector 结尾的元素是一个严重错误的时候,使程序崩溃
  • get 方法,当传递一个数组外的索引时,它不会 panic,而是返回 None,(因为是 Option<T> 类型)。
    • 适合当偶尔超出 vector 范围的访问属于正常情况的时候;
    • 同时需要你的代码后面有处理 Some(&element)None 的逻辑。
    • 例如,索引参数来自用户输入,在用户输入过大数字时,程序得到 None 值,程序可以告诉用户当前 vector 元素的数量,并请求他们再输入一个有效值。这样比因为输入错误而使得程序崩溃要友好的多。

vector 的工作方式:在 vector 的结尾增加新元素时,在没有足够空间将所有元素依次相邻存放的情况下(连续内存空间),可能会要求分配新内存并将老的元素拷贝到新的空间中。原有空间会被释放。

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

注意:关于 Vec<T> 类型的更多实现细节,在 https://doc.rust-lang.org/stable/nomicon/vec.html 查看 “The Nomicon”

遍历 vector 中的元素

使用 for 循环依次获取 vector 中的元素

1
2
3
4
5
6
7
8
9
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 是可变数组,所以和数组一样只能存储相同类型的值。

如何存储不同类型的值?

  • 使用枚举;

枚举的成员都被定义为相同的枚举类型

1
2
3
4
5
6
7
8
9
10
11
12
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}

// 定义一个枚举,便能在 vector 中存放不同类型的数据了。对于 Rust 来说,vector 中存放的都是 SpreadsheetCell 枚举类型。
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];

为什么 Rust 需要在编译时知道 vector 中数据的类型?

  1. 因为 Rust 需要知道存储每个元素需要多少内存;
  2. 可以准确的知道 vector 中允许什么类型,防止操作不同类型元素时造成错误;

所以使用枚举 + match匹配,意味着 Rust 能在编译时就保证可以处理所有可能的情况。

TODO ,查看 vec 的标准库 API 文档,了解 vec 的其他用法

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

什么是字符串?

Rust 核心语言中,只有一种字符串类型: str,字符串切片(slice),它通常以被借用的形式出现,即 &str

String 是由标准库(std)提供的,可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。(所以在堆上)

  • String&str 都是 UTF-8 编码的。
  • &str&[u8]

Rust 标准库中还包含一系列其他字符串类型,例如,OsStgringOsStrCString等,更多有关如何使用它们以及各自适合的场景,请参考其 API 文档。

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

新建字符串

  1. 新建空字符串
1
let mut s = String::new();
  1. 从已有数据中创建新字符串类型
    • .to_string() 方法
    • String::from() 关联函数
1
2
3
4
5
6
7
let data = "initial";
let s = data.to_string();

// 同样适用于字符串字面值
let s1 = "initial".to_string();

let s2 = String::from("initial");

更新字符串

String 大小是可变的,内容也可以更改。

  1. 使用 + 运算符,或 format! 宏来拼接 String 值;

    • + 类似于fn add(self, s: &str) -> String { 这样的签名,因为sefl没有使用&。所以+ 号前的变量会获取所有权,而 + 号后的变量仅是获取引用;
    • 使用 format!宏,不会获取任何参数的所有权,类似于 println!
  2. 使用 push_strpush 附加字符串

    • push_str 将一个字符串切片追加(&str 是一种引用,不获取所有权);
    • push 将一个字符追加 .push(ch: char) ,char 是单引号;

例如:使用 push_strpush 的例子

1
2
3
4
5
let mut s = String::from("foo");
s.push_str("bar");

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

例如:

1
2
3
4
5
6
7
8
9
10
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = s1 + "-" + &s2 + "-" + &s3;

返回:"tic-tac-toe"

// 如果是多个字符串连接,可以使用 format! 宏
let s = format!("{}-{}-{}", s1,s2,s3);

为什么 + 运算符可以允许 &strString 相加?而不能将两个 String 值相加?也是因为 add 的签名:

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

上面第二个参数是 &str ,在 add 调用中使用 &str 是因为 &String 可以被强制转换(coerced)成 &str。 Rust 使用了一个被称为解引用强制多态(derefcoercion)的技术,把 &String 变为 &str ,即把 &s2 变为了 &s2[..]

第十五章会更深入的讨论解引用强制多态。

不支持使用索引语法获取字符串的内容

在 Rust 中不允许使用索引语法获取 String 的一部分。

因为字符串索引返回的类型是不明确的,可能是:字节值、字符、字形簇或者字符串切片。

字节、标量值和字形簇

从 Rust 的角度看,有三种相关方式可以理解字符串:字节、标量值和字形簇(最接近人们眼中的字母的概念)。

String 的底层存储使用 Vec<u8> 的形式。,所以存储的是 u8格式,并不是我们眼中的字母。

支持使用字符串切片

因为字符串切片使用的是[]range(一个范围)来创建含特定字节的字符串切片。

但是:如果获取的无效索引范围不能表达一个完整字符,那么 Rust 会在运行是 panic,与访问 vector 中的无效索引一样。所以要小心使用字符串切片获取部分字符串的操作。

遍历字符串的方法

  1. 如果希望操作单独的 Unicode 标量值,最好的选择是使用 chars方法
  2. bytes方法返回每一个原始字节。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
for c in "नमस्ते".chars() {
println!("{}", c);
}

返回 6char 类型的值:







for b in "नमस्ते".bytes() {
println!("{}", b);
}

返回 18个字节:
224
164
// --snip--
165
135

Rust 中必须更多的思考如何预先处理 UTF-8 数据,

  • 这种权衡取舍比其他语言更多的暴露了字符串的复杂性;
  • 不过也使你在开发生命周期后期免于处理涉及非 ASCII 字符的错误。

字符串解析

1
2
3
4
5
6
7
8
9
10
Basic usage

let four: u32 = "4".parse().unwrap();

assert_eq!(4, four);
Using the 'turbofish' instead of annotating four:

let four = "4".parse::<u32>();

assert_eq!(Ok(4), four);

path::<xx>,

method::<xx>()

为表达式中的泛型类型,函数或方法指定参数,有助于推测算法具体了解我们需要解析的类型。

在 Hash 映射中存储键值对

HashMap<K,V> 类型存储了一个键类型 K,对应一个值类型 V 的映射。

通过一个 hash 函数(hashing function)来实现映射,决定如何将键和值放入内存中。

更多方法需要查看标准库文档。

新建一个 HashMap

因为 HashMap 在标准库,但不在 prelude 中,所以使用 HashMap 之前,必须使用 use 将标准库中集合部分的 HashMap 引入当前项目作用域中。

有两种方法

方法一,使用 new关联函数

1
2
3
4
use std::collections::HashMap;

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

创建空 HashMap 后,需要使用 insert 方法插入数据;

方法二:使用一个元组的 vector 的 collect 方法。其中每个元组包含一个键值对。

collect 方法可以将数据收集进一系列的集合方法,包括 HashMap

1
2
3
4
5
6
7
use std::collections::HashMap;

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

// 这里 HashMap<_,_> 类型标注是必要的,因为可能 collect 很多不同的数据结构,除非显示指定,否则 Rust 无从知道你需要的类型。但是对于键值对类型参数来说,可以使用 _ 下划线占位,Rust 能够根据 vector 中数据的类型推断出 HashMap 所包含的类型。
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();

HashMap 和所有权

i32 这样实现了 Copy trait 的类型,其值可以拷贝进 HashMap;

像 String 这样拥有所有权的值,其值将被移动,而 HashMap 会成为这些值的所有者。

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

访问 HashMap 中的值

可以使用 get 方法,通过键来获取值。

  • get 返回 Option<V>,有结果就返回Some<V>,没有结果就返回 None

也可以使用 vector 类似的方式来遍历 HashMap 中的键值对

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

覆盖一个值

insert 方法默认会将同一个key 的原有值覆盖掉;

只在键没有对应值时插入

  1. Entry 会检查是否存在该键值,返回一个枚举。
  2. 然后调用 Entry 的 or_insert 方法。有值就不插入,没有值就插入。
1
2
3
4
5
6
7
8
9
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);

根据旧值更新一个值

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

or_insert 方法事实上返回这个键的值的一个可变引用(&mut V)。我们这里将这个可变引用存储在 count 变量中,所以为了赋值必须首先使用星号(*)解引用 count。然后在 for 循环作用域内完成,这样所有改变都是安全的并复合借用规则的。

作为 HashMap 的Key

任何类型实现了 EqHash 两个 trait 都可以作为 HashMap 的 Key。

  • bool
  • intuint,和其他所有的变化,
  • String&str

f32f64 不能作为 Key,因为不能实现 Hash,原因是浮点数的精度问题,如果作为hashmap的key 非常容易出错。

如果集合类包含的类型也分别实现了Eq和Hash,那么所有集合类都实现了Eq和Hash。例如,如果T实现Hash, Vec将实现Hash。???

自定义类型如何实现 EqHash

只需要如下属性:

#[derive(PartialEq, Eq, Hash)]

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2023 ligongzhao
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信