👈 7 泛型和特征
字符串 String
String
被实现为一个带有一些额外保证、限制和功能的 Vec<u8>
的封装。
创建:
// new()
let s = String::new();
// from()
let s = String::from("Rust");
// to_string()
let s = "Rust".to_string();
附加:
// push_str()
// 接收 &str, 不移动所有权
let mut s = String::from("R");
s.push_str("us"); // "Rus"
// push()
// 接收 char
s.push('t'); // "Rust"
拼接:
// + 运算符
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1 被移动, 不能继续使用
// + 重载了 String::add:
fn add(self, s: &str) -> String {
// 调用时, &s2 由 &String 强转为 &str
// format! 宏
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let ttt = format!("{s1}-{s2}-{s3}");
遍历:
// chars()
for c in "Rust".chars()
// bytes()
for b in "Rust".bytes()
动态数组 Vector
创建
动态数组类型为 Vec<T>
:
let v: Vec<i32> = Vec::new();
在这里显式地标注了 i32
,因为从 Vec:new()
无法推断元素类型,不过可以在创建后向其中加入元素以省去类型标注:
let mut v = Vec::new();
v.push(1);
如果预先知道元素个数,可以调用 Vec::with_capacity(capacity)
,提升性能。
还可以用宏 vec!
创建,这种方式可以接受初始值,同时也无需标注类型:
let v = vec![1, 2, 3];
访问
访问元素时,可以用索引或者 get
:
let v = vec![1, 2, 3];
// 索引
let rval: &i32 = &v[1];
// get
match v.get(1) {
Some(val) => println!("v[1] = {val}"),
None => println!("v[1] does not exist"),
}
// 或者
let some_num = v.get(1).unwrap();
方式 | 越界时 | 解构 |
---|---|---|
索引 | panic | 无需 |
get | 得到 None | 需要 |
在索引可能越界时,应使用 get
访问,否则应使用性能更好的索引访问。
借用
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4);
println!("v[0]={}", first);
这段代码无法过编:
let first = &v[0];
- immutable borrow occurs here
v.push(4);
^^^^^^^^^ mutable borrow occurs here
println!("v[0]={}", first);
----- immutable borrow later used here
解释如下:
let mut v = vec![1, 2, 3];
let first = &v[0]; // 不可变借用 // ok
v.push(4); // 可变借用 // ok
println!("v[0]={}", first); // 再次使用不可变借用 // not ok
这样设计是有道理的:数组的大小是可变的,当旧数组的大小不够用时,Rust 会重新分配一块更大的内存空间,然后把旧数组拷贝过来。这种情况下,之前的引用会指向一块无效的内存。
在长大之后,我们感激人生路上遇到过的严师益友,正是因为他们,我们才在正确的道路上不断前行,虽然在那个时候,并不能理解他们。而 Rust 就如那个良师益友,它不断的在纠正我们不好的编程习惯,直到某一天,你发现自己能写出一次性通过的漂亮代码时,就能明白它的良苦用心。
迭代遍历
对 Vector
的迭代遍历更加安全、高效:
let mut v = vec![1, 2, 3];
// 不可变迭代遍历
for x in &v {
// ...
}
// 可变迭代遍历
for x in &mut v {
*x += 10;
// ...
}
存储不同类型的元素
枚举实现:
#[derive(Debug)]
enum IpAddr {
V4(String),
V6(String),
}
fn main() {
let ips = vec![
IpAddr::V4("127.0.0.1".to_string()),
IpAddr::V6("::1".to_string()),
];
for ip in ips {
display_ip(ip)
}
}
fn display_ip(ip: IpAddr) {
println!("{:?}", ip);
}
将 V4
、V6
类型的元素存储在枚举 IpAddr
中,再将枚举的实例存储在动态数组 ips
中。
特征对象实现:
trait IpAddr {
fn display(&self);
}
struct V4(String);
impl IpAddr for V4 {
fn display(&self) {
println!("ipv4: {:?}", self.0)
}
}
struct V6(String);
impl IpAddr for V6 {
fn display(&self) {
println!("ipv6: {:?}", self.0)
}
}
fn main() {
let ips: Vec<Box<dyn IpAddr>> = vec![
Box::new(V4("127.0.0.1".to_string())),
Box::new(V6("::1".to_string())),
];
for ip in ips {
ip.display();
}
}
为结构体 V4
和 V6
实现特征 IpAddr
,ips
的元素类型为 Box<dyn IpAddr>
,意为用 Box
包裹的特征 IpAddr
对象。
特征对象动态数组更灵活,也更常见。
排序
Rust 的库中有两种排序:稳定的 sort
、sort_by
,不稳定的 sort_unstable
、sort_unstable_by
。
稳定排序的缺点在于需要额外分配原数组一半大小的空间,且速度更慢。
对 Vec<i32>
排序:
let mut i32s = vec![1, 10, 4, 7];
i32s.sort_unstable();
assert_eq!(i32s, vec![1, 4, 7, 10]); // success
类似地,尝试对 Vec<f32>
排序:
// not ok
let mut f32s = vec![1.3, 8.9, 4.5, 7.2];
f32s.sort_unstable();
assert_eq!(f32s, vec![1.3, 4.5, 7.2, 8.9]); // fail
无法过编,因为 f32
有特殊值 NAN
,无法与其他值比较,因此 f32
实现的是部分可比较的特性 PartialOrd
,但排序要求实现全部可比较的特性 Ord
。
需要改为使用 partial_cmp
:
let mut f32s = vec![1.0, 5.6, 10.3, 2.0, 15f32];
f32s.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
assert_eq!(f32s, vec![1.0, 2.0, 5.6, 10.3, 15f32]); // success
对 Vec<struct>
排序:
#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: String, age: u32) -> Person {
Person { name, age }
}
}
fn main() {
let mut people = vec![
Person::new("Zoe".to_string(), 25),
Person::new("Al".to_string(), 60),
Person::new("John".to_string(), 1),
];
// 自定义按年龄倒序排序
people.sort_unstable_by(|a, b| b.age.cmp(&a.age));
println!("{:?}", people);
}
如果为 Person
实现 Ord
特性,就无需自定义排序了。实现 Ord
需要实现 Ord
、Eq
、PartialOrd
、PartialEq
这些属性,但它们都可以 derive
:
#[derive(Debug, Ord, Eq, PartialOrd, PartialEq)]
struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: String, age: u32) -> Person {
Person { name, age }
}
}
fn main() {
let mut people = vec![
Person::new("Zoe".to_string(), 25),
Person::new("Al".to_string(), 60),
Person::new("Al".to_string(), 30),
Person::new("John".to_string(), 1),
Person::new("John".to_string(), 25),
];
people.sort_unstable();
println!("{:?}", people);
}
哈希表 HashMap
创建
使用 new()
创建
不同于 String
或 Vec
等,HashMap
没有包含在 prelude
中,需要手动引入。
use std::collections::HashMap; // 引入
// 创建 HashMap
let mut gems = HashMap::new();
// 填入键值对
gems.insert("ruby", 1);
gems.insert("emerald", 2);
gems.insert("wastestone", 18);
使用迭代器和 collect()
创建
设想有一个 Vec<(String, u32)>
存储了球队名称与得分的元组,现需要将其写入一个 HashMap<String, u32>
。
可以这样手写:
let mut teams_map = HashMap::new();
for team in &teams_list {
teams_map.insert(&team.0, team.1);
}
这很不 Rusty,可以改用 collect
:
let teams_map: HashMap<_, _> = teams_list.into_iter().collect();
into_iter()
将列表转换为迭代器,再用 collect()
收集,后者实际上支持生成多种目标类型的集合,因此需要显式地将 teams_map
标注为 HashMap<_,_>
,表示需要一个 HashMap
,否则会出错:
error[E0282]: type annotations needed
let teams_map = teams_list.into_iter().collect();
^^^^^^^^^
help: consider giving `teams_map` an explicit type
所有权转移
规则是相同的,将一个未实现 Copy
特征的变量传入 HashMap
后,所有权被转移。
let name = String::from("Sunface");
let age = 18;
let mut handsome_boys = HashMap::new();
handsome_boys.insert(name, age);
println!("{name}"); // not ok
如果在 HashMap
中使用引用类型,要确保该引用的生命周期不短于哈希表:
let name = String::from("Sunface");
let age = 18;
let mut handsome_boys = HashMap::new();
handsome_boys.insert(&name, age);
std::mem::drop(name); // not ok // 后文中再次借用
println!("{:?}", handsome_boys);
访问
用 get()
访问元素:
let mut gems = HashMap::new();
gems.insert("ruby", 1);
gems.insert("emerald", 2);
let rubies: Option<&i32> = gems.get("ruby");
用 &i32
借用 value
,防止所有权转移。
也可以直接获得值类型:
let rubies: i32 = gems.get("ruby").copied().unwrap_or(0);
copied()
通过复制内容将 Option<&T>
转换为 Option<T>
,unwrap_or()
解构 Option<T>
,如果得到 None
,就返回指定的默认值 0
。
用 for-in
遍历 HashMap
:
for (gem, cnt) in &gems {
// ...
}
更新
let mut gems = HashMap::new();
gems.insert("ruby", 1);
gems.insert("emerald", 2);
// 更新, 同时返回旧值
let rubies_old = gems.insert("ruby", 6);
// rubies_old == Some(1)
// 查询, 不存在时插入
let sapphires = gems.entry("sapphire").or_insert(0);
// *sapphires == 0
let emeralds = gems.entry("emerald").or_insert(1);
// *emeralds == 2
or_insert()
返回 &mut v
引用,可以通过该可变引用直接修改 HashMap
中对应的值,例如:
let cnt = gems.entry("ruby").or_insert(0);
*cnt += 1; // gems[ruby] == 1
要对 cnt
先解引用再更新,否则类型不匹配。等价 Java 代码:
map.merge(key, 1, Integer::sum);
哈希函数
一个类型能否作为 key
的关键在于能否进行相等比较(能否确认相同和区分不同),对应的特征为 std::cmp::Eq
。
f32
和f64
由于NAN
的存在,没有实现std::cmp::Eq
,因此无法作为key
。
哈希冲突越少,哈希函数越安全。
目前,HashMap
使用的哈希函数是 SipHash
,它的性能一般,而安全性很好。实现算法时,可以考虑性能更好的哈希函数,例如 ahash
。
👉 9 生命周期