跳到主要内容

集合类型

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)返回 NoneOption<&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);
}

对比

特性&strString
内存位置栈或二进制文件
可变性不可变可变
大小固定可增长
所有权借用拥有

创建 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>索引访问快,末尾添加快
存储字符串StringUTF-8 编码,可增长
键值对映射HashMap<K, V>快速查找
需要有序键值对BTreeMap<K, V>键有序排列
双端队列VecDeque<T>头尾都高效添加删除
去重集合HashSet<T>快速判断元素是否存在
有序去重集合BTreeSet<T>元素有序排列

小结

本章我们学习了:

  1. Vector:动态数组,存储多个相同类型的值
  2. String:UTF-8 编码的可增长字符串
  3. HashMap:键值对存储,快速查找

练习

  1. 创建一个 Vector,存储 1 到 10 的整数,计算平均值
  2. 给定一个字符串,统计每个字符出现的次数
  3. 使用 HashMap 实现一个简单的通讯录(姓名 -> 电话号码)
  4. 实现一个函数,接受字符串并返回第一个单词

参考资源