集合类型
Rust 标准库提供了多种集合类型,用于存储多个值。与内置的数组和元组不同,集合类型的数据存储在堆上,可以在运行时动态增长或缩小。本章将介绍三种最常用的集合类型:Vector、String 和 HashMap。
集合概述
集合类型是 Rust 标准库中非常重要的数据结构。与数组不同,集合的大小不需要在编译时确定,可以根据需要动态调整。
┌─────────────────────────────────────────────────────────────┐
│ Rust 常用集合类型 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Vec<T> String HashMap<K, V> │
│ 动态数组 字符串 哈希映射 │
│ ────────── ────────── ────────────── │
│ 存储多个 存储 UTF-8 键值对存储 │
│ 相同类型值 编码文本 快速查找 │
│ │
│ 特点: │
│ - 数据存储在堆上 │
│ - 大小可以动态变化 │
│ - 拥有数据所有权 │
│ │
└─────────────────────────────────────────────────────────────┘
Vector 动态数组
Vector(Vec<T>)是一种动态数组,可以存储多个相同类型的值,且大小可以动态变化。
创建 Vector
fn main() {
// 创建空 Vector(需要类型注解)
let v: Vec<i32> = Vec::new();
// 使用 vec! 宏创建带初始值的 Vector
let v = vec![1, 2, 3, 4, 5];
// 使用 with_capacity 预分配容量(提高性能)
let v: Vec<i32> = Vec::with_capacity(100);
println!("Vector: {:?}", v);
}
解释:
Vec::new()创建空的 Vector,需要显式指定类型vec!宏可以方便地创建带初始值的 Vector,类型会自动推断with_capacity可以预分配内存,避免频繁重新分配
更新 Vector
fn main() {
let mut v = Vec::new();
// 使用 push 添加元素
v.push(1);
v.push(2);
v.push(3);
println!("Vector: {:?}", v);
// 在指定位置插入
v.insert(1, 10); // 在索引 1 处插入 10
println!("插入后: {:?}", v);
// 删除最后一个元素
let last = v.pop();
println!("弹出: {:?}, 剩余: {:?}", last, v);
// 删除指定位置的元素
let removed = v.remove(0);
println!("删除: {}, 剩余: {:?}", removed, v);
}
读取元素
Vector 提供两种方式读取元素:索引访问和 get 方法。
fn main() {
let v = vec![1, 2, 3, 4, 5];
// 方式一:索引访问(越界会 panic)
let third = &v[2];
println!("第三个元素: {}", third);
// 方式二:get 方法(返回 Option)
match v.get(2) {
Some(value) => println!("第三个元素: {}", value),
None => println!("没有第三个元素"),
}
// 越界访问对比
// let does_not_exist = &v[100]; // panic!
let does_not_exist = v.get(100); // 返回 None
println!("越界访问: {:?}", does_not_exist);
}
两种方式的区别:
| 方式 | 越界行为 | 返回类型 | 适用场景 |
|---|---|---|---|
&v[i] | panic | &T | 确信索引有效时 |
v.get(i) | 返回 None | Option<&T> | 索引可能无效时 |
所有权和借用规则
Vector 遵循 Rust 的所有权和借用规则:
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
// 获取第一个元素的引用
let first = &v[0];
// 错误!不能同时存在可变和不可变引用
// v.push(6); // 编译错误
// println!("第一个元素: {}", first);
// 正确做法:先使用引用,再修改
println!("第一个元素: {}", first);
v.push(6);
println!("添加后: {:?}", v);
}
为什么这个规则很重要?
Vector 在内存中连续存储元素。当添加新元素时,如果当前空间不足,Vector 会重新分配更大的内存空间并移动所有元素。此时,旧的引用将指向已释放的内存,导致未定义行为。Rust 的借用规则在编译时防止了这种问题。
遍历 Vector
fn main() {
let v = vec![100, 32, 57];
// 不可变遍历
println!("遍历元素:");
for i in &v {
println!("{}", i);
}
// 可变遍历
let mut v2 = vec![100, 32, 57];
for i in &mut v2 {
*i += 50; // 使用解引用修改值
}
println!("修改后: {:?}", v2);
// 消费遍历(获取所有权)
let v3 = vec![1, 2, 3];
for i in v3 {
println!("消费: {}", i);
}
// v3 不再可用
}
使用枚举存储多种类型
Vector 只能存储相同类型的元素,但可以通过枚举来存储不同类型:
#[derive(Debug)]
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
fn main() {
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("蓝色")),
SpreadsheetCell::Float(10.12),
];
for cell in &row {
match cell {
SpreadsheetCell::Int(i) => println!("整数: {}", i),
SpreadsheetCell::Float(f) => println!("浮点数: {}", f),
SpreadsheetCell::Text(s) => println!("文本: {}", s),
}
}
}
常用方法
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
// 长度和容量
println!("长度: {}, 容量: {}", v.len(), v.capacity());
// 判断是否为空
println!("是否为空: {}", v.is_empty());
// 是否包含某值
println!("包含 3: {}", v.contains(&3));
// 清空
v.clear();
println!("清空后: {:?}", v);
// 扩展
v.extend([1, 2, 3]);
println!("扩展后: {:?}", v);
// 追加另一个 Vector
let mut v2 = vec![4, 5, 6];
v.append(&mut v2);
println!("追加后 v: {:?}, v2: {:?}", v, v2);
// 截断
v.truncate(3);
println!("截断后: {:?}", v);
// 排序
let mut nums = vec![3, 1, 4, 1, 5, 9];
nums.sort();
println!("排序后: {:?}", nums);
// 反转
nums.reverse();
println!("反转后: {:?}", nums);
}
String 字符串
String 是一个 UTF-8 编码的可增长字符串类型。Rust 中的字符串比其他语言更复杂,但这也带来了更好的安全性。
String 和 &str 的区别
fn main() {
// &str:字符串切片(不可变,通常是借用)
let s1: &str = "hello"; // 字符串字面量
// String:堆分配的可增长字符串
let s2: String = String::from("hello");
println!("&str: {}", s1);
println!("String: {}", s2);
}
对比:
| 特性 | &str | String |
|---|---|---|
| 内存位置 | 栈或二进制文件 | 堆 |
| 可变性 | 不可变 | 可变 |
| 大小 | 固定 | 可增长 |
| 所有权 | 借用 | 拥有 |
创建 String
fn main() {
// 从字面量创建
let s1 = String::from("hello");
// 使用 to_string 方法
let s2 = "world".to_string();
// 创建空字符串
let s3 = String::new();
// 从字符创建
let s4: String = ['h', 'e', 'l', 'l', 'o'].iter().collect();
// 使用 with_capacity 预分配
let s5 = String::with_capacity(100);
println!("s1: {}, s2: {}, s3: '{}', s4: {}", s1, s2, s3, s4);
}
更新 String
fn main() {
let mut s = String::from("hello");
// 追加字符串
s.push_str(" world");
println!("追加后: {}", s);
// 追加单个字符
s.push('!');
println!("添加字符后: {}", s);
// 使用 + 运算符拼接(会转移所有权)
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1 被移动,s2 被借用
println!("拼接结果: {}", s3);
// println!("{}", s1); // 错误!s1 已被移动
println!("s2 仍可用: {}", s2);
// 使用 format! 宏拼接(不会转移所有权)
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3);
println!("format 结果: {}", s);
println!("s1, s2, s3 仍可用: {}, {}, {}", s1, s2, s3);
}
索引字符串
Rust 不支持直接使用索引访问字符串,因为 UTF-8 编码的字符可能占用多个字节。
fn main() {
let s = String::from("hello");
// 错误!Rust 不支持字符串索引
// let h = s[0];
// 正确方式:使用切片
let hello = &s[0..5]; // 获取字节范围
println!("切片: {}", hello);
// 注意:切片是按字节而非字符
let chinese = String::from("你好");
// let slice = &chinese[0..1]; // 危险!可能破坏 UTF-8 字符
let slice = &chinese[0..3]; // "你" 占 3 个字节
println!("中文切片: {}", slice);
}
遍历字符串
fn main() {
let s = String::from("你好世界");
// 遍历字符
println!("遍历字符:");
for c in s.chars() {
println!("{}", c);
}
// 遍历字节
println!("\n遍历字节:");
for b in s.bytes() {
println!("{}", b);
}
// 遍历字符及其索引
println!("\n遍历字符和索引:");
for (i, c) in s.char_indices() {
println!("索引 {}: 字符 {}", i, c);
}
}
常用方法
fn main() {
let mut s = String::from("Hello, World!");
// 长度(字节数)
println!("字节长度: {}", s.len());
// 字符数
println!("字符数: {}", s.chars().count());
// 判断是否为空
println!("是否为空: {}", s.is_empty());
// 分割
let parts: Vec<&str> = s.split(", ").collect();
println!("分割: {:?}", parts);
// 替换
let replaced = s.replace("World", "Rust");
println!("替换后: {}", replaced);
// 大小写转换
println!("小写: {}", s.to_lowercase());
println!("大写: {}", s.to_uppercase());
// 去除空白
let s2 = " hello ";
println!("去除两端空白: '{}'", s2.trim());
// 判断开头和结尾
println!("以 Hello 开头: {}", s.starts_with("Hello"));
println!("以 ! 结尾: {}", s.ends_with("!"));
// 包含
println!("包含 World: {}", s.contains("World"));
}
字符串和所有权
fn main() {
let s1 = String::from("hello");
// 函数参数:建议使用 &str
fn print_str(s: &str) {
println!("{}", s);
}
// 可以接受 &String(自动解引用为 &str)
print_str(&s1);
// 也可以接受 &str
print_str("world");
// 函数返回 String
fn create_string() -> String {
String::from("new string")
}
let s2 = create_string();
println!("返回的字符串: {}", s2);
}
HashMap 哈希映射
HashMap 存储键值对,通过键快速查找对应的值。
创建 HashMap
use std::collections::HashMap;
fn main() {
// 创建空 HashMap
let mut scores: HashMap<String, i32> = HashMap::new();
// 插入键值对
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
println!("分数: {:?}", scores);
// 从两个 Vector 创建
let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.into_iter().zip(initial_scores.into_iter()).collect();
println!("从 Vector 创建: {:?}", scores);
}
访问值
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// 使用 get 方法(返回 Option)
let team_name = String::from("Blue");
match scores.get(&team_name) {
Some(score) => println!("Blue 队分数: {}", score),
None => println!("队伍不存在"),
}
// 使用 unwrap_or 提供默认值
let score = scores.get(&String::from("Red")).unwrap_or(&0);
println!("Red 队分数: {}", score);
// 遍历
for (key, value) in &scores {
println!("{}: {}", key, value);
}
}
更新 HashMap
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
// 覆盖旧值
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25); // 覆盖
println!("覆盖后: {:?}", scores);
// 只在键不存在时插入
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(100); // 不会插入
println!("entry 后: {:?}", scores);
// 基于旧值更新
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);
}
删除元素
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// 删除键值对
scores.remove(&String::from("Blue"));
println!("删除后: {:?}", scores);
// 清空
scores.clear();
println!("清空后: {:?}", scores);
}
所有权
HashMap 对于实现了 Copy trait 的类型会复制值,对于拥有所有权的类型(如 String)会移动所有权:
use std::collections::HashMap;
fn main() {
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 已被移动
// println!("{}", field_name); // 错误!
// 使用引用(需要确保引用有效)
let name = String::from("Alice");
let age = 30;
let mut map2 = HashMap::new();
map2.insert(&name, age); // name 被借用,age 被复制
println!("name 仍可用: {}", name);
println!("map2: {:?}", map2);
}
常用方法
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert("a", 1);
map.insert("b", 2);
map.insert("c", 3);
// 长度
println!("长度: {}", map.len());
// 判断是否为空
println!("是否为空: {}", map.is_empty());
// 判断是否包含键
println!("包含 a: {}", map.contains_key("a"));
// 获取键和值的集合
println!("所有键: {:?}", map.keys().collect::<Vec<_>>());
println!("所有值: {:?}", map.values().collect::<Vec<_>>());
// 遍历
for (k, v) in &map {
println!("{}: {}", k, v);
}
// 保留满足条件的元素
map.retain(|&k, &mut _v| k != "b");
println!("retain 后: {:?}", map);
}
集合类型选择指南
| 场景 | 推荐类型 | 说明 |
|---|---|---|
| 存储有序列表 | Vec<T> | 索引访问快,末尾添加快 |
| 存储字符串 | String | UTF-8 编码,可增长 |
| 键值对映射 | HashMap<K, V> | 快速查找 |
| 需要有序键值对 | BTreeMap<K, V> | 键有序排列 |
| 双端队列 | VecDeque<T> | 头尾都高效添加删除 |
| 去重集合 | HashSet<T> | 快速判断元素是否存在 |
| 有序去重集合 | BTreeSet<T> | 元素有序排列 |
小结
本章我们学习了:
- Vector:动态数组,存储多个相同类型的值
- String:UTF-8 编码的可增长字符串
- HashMap:键值对存储,快速查找
练习
- 创建一个 Vector,存储 1 到 10 的整数,计算平均值
- 给定一个字符串,统计每个字符出现的次数
- 使用 HashMap 实现一个简单的通讯录(姓名 -> 电话号码)
- 实现一个函数,接受字符串并返回第一个单词