Rust快速入门(6) - 常见集合

Rust标准库提供了许多有用的数据结构,称为集合(collections)。与内置的数组和元组不同,这些集合指向的数据存储在堆上,这意味着数据的大小可以在运行时增长或缩小。

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

1. 向量(Vec)

向量允许我们在内存中存储多个相同类型的值,它们彼此相邻。向量只能存储相同类型的值。

1.1 创建向量

创建空向量的方法:

// 指定类型
let v: Vec<i32> = Vec::new();

// 使用宏创建带初始值的向量(类型自动推断)
let v = vec![1, 2, 3];

1.2 更新向量

向量是可变的,可以添加或移除元素:

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

1.3 读取向量元素

有两种方法可以读取向量中的值:

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

// 1. 使用索引语法(越界会导致程序崩溃)
let third: &i32 = &v[2];

// 2. 使用get方法(返回Option,更安全)
let third: Option<&i32> = v.get(2);
match third {
    Some(third) => println!("第三个元素是 {}", third),
    None => println!("没有第三个元素"),
}

向量的所有权和借用规则与其他Rust类型相同。不能在持有向量元素引用的同时修改向量,因为向量在内存中是连续存储的,添加新元素可能需要分配新内存并移动所有元素。

1.4 遍历向量元素

// 不可变遍历
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; // 需要使用解引用操作符
}

1.5 使用枚举存储多种类型

向量只能存储同一类型的元素,但可以使用枚举来存储不同类型的值:

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

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

1.6 删除向量

向量在离开作用域时会被丢弃,它包含的所有元素也会被丢弃:

{
    let v = vec![1, 2, 3];
    // 使用v
} // 这里v离开作用域并被释放

2. 字符串(String)

Rust中的字符串比许多程序员想象的更为复杂,这主要与Rust如何处理内存、暴露可能的错误以及字符串是UTF-8编码有关。

2.1 什么是字符串?

Rust核心语言中只有一种字符串类型:字符串切片str,通常以借用形式&str出现。

String类型是由标准库提供的,它是可增长的、可变的、有所有权的、UTF-8编码的字符串类型。

2.2 创建字符串

创建新的空字符串:

let mut s = String::new();

从字符串字面量创建字符串:

// 使用to_string方法
let s = "initial contents".to_string();

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

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

let hello = String::from("你好");

2.3 更新字符串

字符串可以增长,内容可以更改:

// 使用push_str添加字符串切片
let mut s = String::from("foo");
s.push_str("bar");  // s现在包含"foobar"

// 使用push添加单个字符
let mut s = String::from("lo");
s.push('l');  // s现在包含"lol"

2.3.1 字符串连接

可以使用+运算符或format!宏连接字符串:

// 使用+运算符(注意s1被移动了)
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;  // s1不再有效

// 使用format!宏(更灵活且不获取所有权)
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"

2.4 字符串索引

Rust不支持使用索引语法访问字符串中的字符:

let s1 = String::from("hello");
let h = s1[0];  // 错误!

这是因为:

  1. 字符串内部是一个Vec<u8>的包装
  2. 一些UTF-8编码的字符可能占用多个字节
  3. 字符串索引操作应该返回什么并不明确(字节值、字符、字形簇?)
  4. 索引操作的时间复杂度应该是O(1),但对于UTF-8字符串这无法保证

2.5 字符串切片

可以使用范围创建字符串切片,但要确保切片边界是有效的字符边界:

let hello = "Здравствуйте";
let s = &hello[0..4];  // s将包含前两个字符"Зд"

// 越界或切分字符将导致程序崩溃
let s = &hello[0..1];  // 错误!这不是字符边界

2.6 遍历字符串

更安全的处理字符串的方法是明确指定要操作的是字符还是字节:

// 遍历字符
for c in "नमस्ते".chars() {
    println!("{}", c);
}

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

获取字形簇比较复杂,标准库不提供此功能,需要使用外部crate。

3. 哈希映射(HashMap)

哈希映射(HashMap)允许我们存储键值对的映射。许多编程语言支持这种数据结构,但可能使用不同的名称,如哈希、映射、对象、哈希表、字典或关联数组。

3.1 创建哈希映射

可以使用new创建空的哈希映射,然后使用insert添加元素:

use std::collections::HashMap;

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

与向量不同,哈希映射没有类似vec!的宏来构建。

3.2 访问哈希映射中的值

使用get方法获取值:

let team_name = String::from("Blue");
let score = scores.get(&team_name);  // 返回Option<&V>

遍历哈希映射:

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

3.3 哈希映射和所有权

对于实现了Copy特性的类型(如i32),值会被复制到哈希映射中。对于持有所有权的值(如String),值会被移动,哈希映射将拥有这些值:

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在这里已经无效

3.4 更新哈希映射

当键已经存在时,处理值的几种方式:

3.4.1 覆盖原值

简单地再次插入同一键的新值会覆盖旧值:

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);  // Blue的值现在是25

3.4.2 只在键不存在时插入

使用entry方法检查键是否存在,只在不存在时插入值:

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

3.4.3 根据旧值更新

查找键的值并根据旧值更新它:

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;  // 需要解引用
}

3.5 哈希函数

默认情况下,HashMap使用SipHash哈希函数,它能提供抵抗拒绝服务(DoS)攻击的能力。这不是最快的哈希算法,但安全性更高。如果性能是关键因素,可以切换到其他哈希函数。

总结

向量、字符串和哈希映射提供了在程序中存储、访问和修改数据的大量功能:

  1. 向量(Vec)允许我们存储可变数量的值
  2. 字符串(String)是字符的集合,使用UTF-8编码,处理起来比其他语言更复杂
  3. 哈希映射(HashMap)允许我们将值与特定键关联起来

这些集合都在堆上存储数据,这意味着数据量不需要在编译时确定,而是可以在程序运行时增长或缩小。每种集合都有不同的能力和成本,为特定情况选择合适的集合是一项随时间发展的技能。


好好学习,天天向上