开始之前,我们先来看下上一文的答案:
fn min<T:Ord>(a:T, b:T) -> T {
if a < b {
a
} else {
b
}
}
非常简单,不是吗?
今天,我们讲 Rust 中的标准库类型。
Rust 附带了一个标准库,该标准库有助于确立一套 Rust 库和程序常用的类型。如此一来,任意两个库就能顺利协作,因为它们都使用相同的 String
类型。
事实上,Rust 包含几层标准库:core
、alloc
和 std
。
core
包含最基本的类型和函数,这些类型和函数不依赖于libc
、分配器,甚至不依赖于操作系统。alloc
包含需要全局堆分配器的类型,如Vec
、Box
和Arc
。- 嵌入式 Rust 应用程序通常只使用
core
,有时也使用alloc
。
文档
Rust 有丰富详尽的文档。例如:
- 有关循环的所有细节。
- 像
u8
这样的基本类型。 - 像 Option 或
BinaryHeap
这样的标准库类型。
使用 rustup doc --std
或访问 std.rs 即可查看文档。
实际上,你也可以为自己的代码撰写文档:
/// Determine whether the first argument is divisible by the second argument.
///
/// If the second argument is zero, the result is false.
fn is_divisible_by(lhs: u32, rhs: u32) -> bool {
if rhs == 0 {
return false;
}
lhs % rhs == 0
}
内容将按 Markdown 格式处理。所有发布的 Rust 库包都会使用 rustdoc
工具在 docs.rs 上自动生成文档。按照习惯,要用这种模式为 API 中的所有公共项编写文档。
要在项目内部(例如在模块内部)为项目添加文档说明,可以使用 //!
或 /*!.. */
,这被称为 “内部文档注释”:
//! This module contains functionality relating to divisibility of integers.
Option
我们已经见识过 Option<T>
的一些用法。它要么存储一个 T
类型的值,要么不存储任何值。例如,String::find
方法会返回一个 Option<usize>
。
fn main() {
let name = "Löwe 老虎 Léopard Gepardi";
let mut position: Option<usize> = name.find('é');
dbg!(position);
assert_eq!(position.unwrap(), 14);
position = name.find('Z');
dbg!(position);
assert_eq!(position.expect("Character not found"), 0);
}
Rust 没有 NULL
,如果我们想表达一个东西可空,用 Option<T>
。
Option
类型的应用非常广泛,并不局限于标准库中。
unwrap
方法会返回 Option
中的值,如果没有值则会导致程序 panic
。expect
方法类似,但需要传入一条错误信息。
你可以在遇到 None
时引发程序 panic
,但不能 “不小心” 忘记检查 None
的情况(想象一下在 Kotlin 中,你的程序因为异常崩溃。表面上崩溃的是你的程序,实际是你的心情)。
在临时拼凑代码时,到处使用 unwrap
或 expect
是很常见的,但在生产代码中,通常会以更优雅的方式处理 None
情况。
Result
Result
与 Option
类似,但它用于表示操作的成功或失败,通过不同的枚举变体来区分。它是泛型类型:Result<T, E>
,其中 T
用于 Ok
变体,而 E
出现在 Err
变体中。
use std::fs::File;
use std::io::Read;
fn main() {
let file: Result<File, std::io::Error> = File::open("diary.txt");
match file {
Ok(mut file) => {
let mut contents = String::new();
if let Ok(bytes) = file.read_to_string(&mut contents) {
println!("Dear diary: {contents} ({bytes} bytes)");
} else {
println!("Could not read file content");
}
}
Err(err) => {
println!("The diary could not be opened: {err}");
}
}
}
与 Option
类似,成功的值存储在 Result
内部,这就要求开发人员显式地提取它。这有助于进行错误检查。在错误永远不应该发生的情况下,可以调用 unwrap()
或 expect()
,这也表明了开发人员的意图。
建议阅读 Result
的文档,它包含很多实用的方法和函数,有助于进行函数式风格的编程。
在 Rust 中,Result
是用于实现错误处理的标准类型。
String
String
是一个可增长的 UTF-8 编码字符串:
fn main() {
let mut s1 = String::new();
s1.push_str("Hello");
println!("s1: len = {}, capacity = {}", s1.len(), s1.capacity());
let mut s2 = String::with_capacity(s1.len() + 1);
s2.push_str(&s1);
s2.push('!');
println!("s2: len = {}, capacity = {}", s2.len(), s2.capacity());
let s3 = String::from("🇨🇭");
println!("s3: len = {}, number of chars = {}", s3.len(), s3.chars().count());
}
// Output
// s1: len = 5, capacity = 8
// s2: len = 6, capacity = 6
// s3: len = 8, number of chars = 2
String
实现了 Deref<Target = str>
,这意味着你可以在 String
上调用所有 str
的方法。
String::new
会返回一个新的空字符串。如果你知道要向字符串中添加多少数据,可以使用 String::with_capacity
。
String::len
返回字符串的字节大小(即该字符串占用空间的大小,这与字符串长度意义是不同的)。
String::chars
返回一个遍历实际字符的迭代器。请注意,由于字符簇的存在,一个 char
可能与我们通常所认为的 “字符” 有所不同。
当开发者提到字符串时,他们可能指的是 &str
或 String
。
当一个类型实现了 Deref<Target = T>
,编译器允许你透明地调用 T
的方法。
- 我们还没有讨论
Deref
特性,所以目前这主要是为了解释文档侧边栏的结构。 String
实现了Deref<Target = str>
,这使得它可以透明地访问str
的方法。
String
实现为字节向量的包装器,许多在向量上支持的操作在 String
上也受支持,但有一些额外的保证。
访问 String
的不同方式:
- 通过
s3.chars().nth(i).unwrap()
访问字符,其中i
在边界内或越界。 - 通过
s3[0..4]
访问子字符串,无论该子字符串是否处于字符边界。
许多类型可以使用 to_string
方法转换为字符串。所有实现了 Display
的类型都会自动实现该特性,所以任何可以格式化的内容都可以转换为字符串。
Vec
Vec
是标准的可调整大小的堆分配缓冲区(类似 Kotlin 中的 List
):
fn main() {
let mut v1 = Vec::new();
v1.push(42);
println!("v1: len = {}, capacity = {}", v1.len(), v1.capacity());
let mut v2 = Vec::with_capacity(v1.len() + 1);
v2.extend(v1.iter());
v2.push(9999);
println!("v2: len = {}, capacity = {}", v2.len(), v2.capacity());
// 用于使用元素初始化向量的宏
let mut v3 = vec![0, 0, 1, 2, 3, 4];
// 只保留偶数元素
v3.retain(|x| x % 2 == 0);
println!("{v3:?}");
// 删除连续的重复项
v3.dedup();
println!("{v3:?}");
}
// Output
// v1: len = 1, capacity = 4
// v2: len = 2, capacity = 2
// [0, 0, 2, 4]
// [0, 2, 4]
Vec
实现了 Deref<Target = [T]>
,这意味着你可以在 Vec
上调用切片方法。
Vec
与 String
和 HashMap
一样,是一种集合类型。它包含的数据存储在堆上。这意味着在编译时不需要知道数据量的大小。它可以在运行时增长或收缩。
注意,Vec<T>
也是一种泛型类型,但你不必显式指定 T
。与 Rust 的类型推断一贯做法一样,T
是在第一次调用 push
时确定的。
vec![...]
是一个宏,可替代 Vec::new()
使用,它支持向向量中添加初始元素(还记得 Kotlin 中的 listOf
吗?)。
要对向量进行索引,可以使用 []
,但如果越界会导致程序崩溃。使用 get
方法会返回一个 Option
。pop
函数会删除最后一个元素。
HashMap
具有防哈希拒绝服务(HashDoS)攻击保护的标准库 HashMap
:
use std::collections::HashMap;
fn main() {
let mut page_counts = HashMap::new();
page_counts.insert("Adventures of Huckleberry Finn", 207);
page_counts.insert("Grimms' Fairy Tales", 751);
page_counts.insert("Pride and Prejudice", 303);
if !page_counts.contains_key("Les Misérables") {
println!(
"We know about {} books, but not Les Misérables.",
page_counts.len()
);
}
for book in ["Pride and Prejudice", "Alice's Adventure in Wonderland"] {
match page_counts.get(book) {
Some(count) => println!("{book}: {count} pages"),
None => println!("{book} is unknown."),
}
}
// 使用 .entry() 方法在未找到值时插入一个值。
for book in ["Pride and Prejudice", "Alice's Adventure in Wonderland"] {
let page_count: &mut i32 = page_counts.entry(book).or_insert(0);
*page_count += 1;
}
dbg!(page_counts);
}
HashDos 是一种攻击方式,攻击者通过精心构造输入数据,利用哈希算法的碰撞特性(即不同输入产生相同哈希值的情况),导致 HashMap
存储的数据退化成链表,系统资源被大量消耗,从而无法正常处理合法请求。Rust 标准库的 HashMap
可以防止此种攻击。
HashMap
未在预导入模块中定义,需要引入作用域,即导入相关的包/库。
试试以下几行代码。第一行会检查某个书籍是否在哈希映射中,如果不存在,则返回一个替代值。第二行会在未找到该书籍时将替代值插入哈希映射中。
let pc1 = page_counts
.get("Harry Potter and the Sorcerer's Stone")
.unwrap_or(&336);
let pc2 = page_counts
.entry("The Hunger Games")
.or_insert(374);
与 vec!
不同,HashMap
没有标准的 hashmap!
宏。
不过,从 Rust 1.56 版本开始,HashMap
实现了 From<[(K, V); N]>
,这使我们能够轻松地从字面值数组初始化哈希映射:
let page_counts = HashMap::from([
("Harry Potter and the Sorcerer's Stone".to_string(), 336),
("The Hunger Games".to_string(), 374),
]);
或者,HashMap
可以从任何生成键值对元组的迭代器中构建而来。
练习
本次练习中,你将把一个非常简单的数据结构泛型化。它使用 std::collections::HashMap
来跟踪哪些值已经被看到过,以及每个值出现的次数。
Counter
的初始版本是硬编码的,只能处理 u32
类型的值。将其泛型化,这样 Counter
就可以跟踪任何类型的值。
use std::collections::HashMap;
/// Counter 统计类型为 T 的每个值出现的次数。
struct Counter {
values: HashMap<u32, u64>,
}
impl Counter {
/// 创建 Counter
fn new() -> Self {
Counter {
values: HashMap::new(),
}
}
/// 统计给定值的出现次数
fn count(&mut self, value: u32) {
if self.values.contains_key(&value) {
*self.values.get_mut(&value).unwrap() += 1;
} else {
self.values.insert(value, 1);
}
}
/// 返回给定值出现的次数
fn times_seen(&self, value: u32) -> u64 {
self.values.get(&value).copied().unwrap_or_default()
}
}
fn main() {
let mut ctr = Counter::new();
ctr.count(13);
ctr.count(14);
ctr.count(16);
ctr.count(14);
ctr.count(14);
ctr.count(11);
for i in 10..20 {
println!("saw {} values equal to {}", ctr.times_seen(i), i);
}
let mut strctr = Counter::new();
strctr.count("apple");
strctr.count("orange");
strctr.count("apple");
println!("got {} apples", strctr.times_seen("apple"));
}