Rust标准库提供了许多有用的数据结构,称为集合(collections)。与内置的数组和元组不同,这些集合指向的数据存储在堆上,这意味着数据的大小可以在运行时增长或缩小。
String
类型,不过在本章我们将深入了解。向量允许我们在内存中存储多个相同类型的值,它们彼此相邻。向量只能存储相同类型的值。
创建空向量的方法:
// 指定类型
let v: Vec<i32> = Vec::new();
// 使用宏创建带初始值的向量(类型自动推断)
let v = vec![1, 2, 3];
向量是可变的,可以添加或移除元素:
let mut v = Vec::new();
v.push(5);
v.push(6);
有两种方法可以读取向量中的值:
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类型相同。不能在持有向量元素引用的同时修改向量,因为向量在内存中是连续存储的,添加新元素可能需要分配新内存并移动所有元素。
// 不可变遍历
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; // 需要使用解引用操作符
}
向量只能存储同一类型的元素,但可以使用枚举来存储不同类型的值:
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
向量在离开作用域时会被丢弃,它包含的所有元素也会被丢弃:
{
let v = vec![1, 2, 3];
// 使用v
} // 这里v离开作用域并被释放
Rust中的字符串比许多程序员想象的更为复杂,这主要与Rust如何处理内存、暴露可能的错误以及字符串是UTF-8编码有关。
Rust核心语言中只有一种字符串类型:字符串切片str
,通常以借用形式&str
出现。
String
类型是由标准库提供的,它是可增长的、可变的、有所有权的、UTF-8编码的字符串类型。
创建新的空字符串:
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("你好");
字符串可以增长,内容可以更改:
// 使用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"
可以使用+运算符或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"
Rust不支持使用索引语法访问字符串中的字符:
let s1 = String::from("hello");
let h = s1[0]; // 错误!
这是因为:
Vec<u8>
的包装可以使用范围创建字符串切片,但要确保切片边界是有效的字符边界:
let hello = "Здравствуйте";
let s = &hello[0..4]; // s将包含前两个字符"Зд"
// 越界或切分字符将导致程序崩溃
let s = &hello[0..1]; // 错误!这不是字符边界
更安全的处理字符串的方法是明确指定要操作的是字符还是字节:
// 遍历字符
for c in "नमस्ते".chars() {
println!("{}", c);
}
// 遍历字节
for b in "नमस्ते".bytes() {
println!("{}", b);
}
获取字形簇比较复杂,标准库不提供此功能,需要使用外部crate。
哈希映射(HashMap)允许我们存储键值对的映射。许多编程语言支持这种数据结构,但可能使用不同的名称,如哈希、映射、对象、哈希表、字典或关联数组。
可以使用new
创建空的哈希映射,然后使用insert
添加元素:
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
与向量不同,哈希映射没有类似vec!
的宏来构建。
使用get
方法获取值:
let team_name = String::from("Blue");
let score = scores.get(&team_name); // 返回Option<&V>
遍历哈希映射:
for (key, value) in &scores {
println!("{}: {}", key, value);
}
对于实现了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在这里已经无效
当键已经存在时,处理值的几种方式:
简单地再次插入同一键的新值会覆盖旧值:
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25); // Blue的值现在是25
使用entry
方法检查键是否存在,只在不存在时插入值:
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
查找键的值并根据旧值更新它:
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; // 需要解引用
}
默认情况下,HashMap
使用SipHash哈希函数,它能提供抵抗拒绝服务(DoS)攻击的能力。这不是最快的哈希算法,但安全性更高。如果性能是关键因素,可以切换到其他哈希函数。
向量、字符串和哈希映射提供了在程序中存储、访问和修改数据的大量功能:
这些集合都在堆上存储数据,这意味着数据量不需要在编译时确定,而是可以在程序运行时增长或缩小。每种集合都有不同的能力和成本,为特定情况选择合适的集合是一项随时间发展的技能。
好好学习,天天向上