与标准库类型一样,Rust 也提供一些标准库 trait 可供使用,通过标准库 trait,我们能减少重复劳动,确保代码与 Rust 生态深度兼容,写出更加符合 Rust 习惯的代码。
开始之前,先给出上文答案:
use std::collections::HashMap;
use std::hash::Hash;
struct Counter<T> {
values: HashMap<T, u64>,
}
impl<T: Eq + Hash> Counter<T> {
fn new() -> Self {
Counter { values: HashMap::new() }
}
fn count(&mut self, value: T) {
*self.values.entry(value).or_default() += 1;
}
fn times_seen(&self, value: T) -> 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"));
}
输出如下:
// Output
saw 0 values equal to 10
saw 1 values equal to 11
saw 0 values equal to 12
saw 1 values equal to 13
saw 3 values equal to 14
saw 0 values equal to 15
saw 1 values equal to 16
saw 0 values equal to 17
saw 0 values equal to 18
saw 0 values equal to 19
got 2 apples
比较
Rust 提供对值进行比较的 trait。如果一个类型的所有字段都实现了比较的 trait,那么这个类型可以直接 derive 比较(和 Clone 类似,如果一个类型所有字段都实现了 Clone,那么这个类型可以通过 derive 来快速实现 Clone)。
PartialEq 是一种部分等价关系,有必需实现的 eq 方法和可选的 ne 方法。== 和 != 运算符会调用这些方法:
struct Key {
id: u32,
metadata: Option<String>,
}
impl PartialEq for Key {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
如果你不实现 ne 方法,Rust 会使用默认实现 !self.eq(other)。
PartialEq 可以在不同类型之间实现,但 Eq 不能,因为它具有自反性:
struct Key {
id: u32,
metadata: Option<String>,
}
impl PartialEq<u32> for Key {
fn eq(&self, other: &u32) -> bool {
self.id == *other
}
}
fn main() {
let a = Key { id: 1, metadata: None };
println!("{}", a == 1);
}
// Output
// true
PartialOrd 定义了一种偏序关系,有 partial_cmp 方法。它用于实现 <、<=、>= 和 > 运算符:
use std::cmp::Ordering;
#[derive(Eq, PartialEq)]
struct Citation {
author: String,
year: u32,
}
impl PartialOrd for Citation {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match self.author.partial_cmp(&other.author) {
Some(Ordering::Equal) => self.year.partial_cmp(&other.year),
author_ord => author_ord,
}
}
}
Ord 是一种全序关系,cmp 方法返回 Ordering。
在 Rust 中比较引用时,它会比较所指向事物的值,而不是比较引用本身。这意味着,如果两个不同类型的引用所指向的值相同,那么它们在比较时可能会被视为相等:
fn main() {
let a = "Hello";
let b = String::from("Hello");
assert_eq!(a, b);
}
上述代码中,a 是 &str 类型,而 b 是 String 类型。
在实际应用中,通常是 derive 这些 trait,而不是开发者自己实现它们。
我知道部分开发者可能会问 Eq 与 PartialEq,Ord 与 PartialOrd 有什么区别,放心,后续文章我会讲解。
运算符
运算符重载是通过 std::ops 中的特性来实现的:
#[derive(Debug, Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
impl std::ops::Add for Point {
type Output = Self;
fn add(self, other: Self) -> Self {
Self { x: self.x + other.x, y: self.y + other.y }
}
}
fn main() {
let p1 = Point { x: 10, y: 20 };
let p2 = Point { x: 100, y: 200 };
println!("{p1:?} + {p2:?} = {:?}", p1 + p2);
}
// Output
// Point { x: 10, y: 20 } + Point { x: 100, y: 200 } = Point { x: 110, y: 220 }
为什么 Output 是一个关联类型呢?它能作为方法的类型参数吗?
简而言之:函数的类型参数由调用者控制,而关联类型(比如 Output)由特性的实现者控制。
可能你现在还不太明白这里面的奥义,不过没关系,跟着教程走就行了。
你可以为两种不同的类型实现 Add 特性,例如,impl Add<(i32, i32)> for Point 可以将一个元组加到一个 Point 上:
impl Add<(i32,i32)> for Point {
type Output = Self;
fn add(self, rhs: (i32, i32)) -> Self::Output {
Self { x: self.x + rhs.0, y: self.y + rhs.1 }
}
}
fn main() {
println!("{:?}", Point{x:1, y:2} + (2,4));
}
// Output
// Point { x: 3, y: 6 }
Not 特性(! 运算符)很值得注意,对于整数类型,它会对数字的每一位取反,从算术角度来说,这等同于用 -1 减去该数字:!5 == -6。
From 和 Into
类型通过实现 From 和 Into 来方便进行类型转换。与 as 不同,这些特性对应的是无损且不会出错的转换。
fn main() {
let s = String::from("hello");
let addr = std::net::Ipv4Addr::from([127, 0, 0, 1]);
let one = i16::from(true);
let bigger = i32::from(123_i16);
println!("{s}, {addr}, {one}, {bigger}");
}
当实现了 From 时,Into 会自动实现:
fn main() {
let s: String = "hello".into();
let addr: std::net::Ipv4Addr = [127, 0, 0, 1].into();
let one: i16 = true.into();
let bigger: i32 = 123_i16.into();
println!("{s}, {addr}, {one}, {bigger}");
}
这就是为什么通常只实现 From,因为你的类型也会获得 Into 的实现。
类型转换
Rust 没有隐式类型转换,但支持使用 as 进行显式类型转换。在有定义的地方,这些转换通常遵循 C 语言的语义。
fn main() {
let value: i64 = 1000;
println!("as u16: {}", value as u16);
println!("as i16: {}", value as i16);
println!("as u8: {}", value as u8);
}
// Output
// as u16: 1000
// as i16: 1000
// as u8: 232
在 Rust 中,as 的转换结果总是有明确定义的,并且在不同平台上保持一致。这可能与你对改变符号或转换为较小类型的直觉不符——查看文档并添加注释以确保代码清晰。
使用 as 进行类型转换是一把双刃剑,很容易使用不当,并且在未来的维护工作中,当使用的类型或类型中的值范围发生变化时,它可能成为难以察觉的错误根源。仅在意图是表示无条件截断时(例如,无论高位是什么,使用 as u32 选取 u64 的低 32 位),才适合使用类型转换。
对于不会出错的类型转换(例如从 u32 转换到 u64),相较于 as,更推荐使用 From 或 Into 来确保类型转换确实不会出错。对于可能出错的类型转换,当你希望区分处理能正确转换的值和不能正确转换的值时,可以使用 TryFrom 和 TryInto。
as 类似于C++中的 static_cast。通常不鼓励在可能会丢失数据的情况下使用 as,或者至少应该添加一条解释性注释。
Default
Default 特性为某个类型提供一个默认值:
#[derive(Debug, Default)]
struct Derived {
x: u32,
y: String,
z: Implemented,
}
#[derive(Debug)]
struct Implemented(String);
impl Default for Implemented {
fn default() -> Self {
Self("John Smith".into())
}
}
fn main() {
let default_struct = Derived::default();
dbg!(default_struct);
let almost_default_struct =
Derived { y: "Y is set!".into(), ..Derived::default() };
dbg!(almost_default_struct);
let nothing: Option<Derived> = None;
dbg!(nothing.unwrap_or_default());
}
和 Clone,PartialEq 类似,如果一个类型里面的所有字段都实现了 Default 特性,那么可以直接使用派生 #[derive(Default)] 来生成实现。当然,你也可以实现自己的 Default 特性。派生的实现会生成一个所有字段都被设置为它们默认值的值。标准 Rust 类型通常使用合理的值实现 Default(例如 0、"" 等)。部分结构体初始化与默认值配合得很好。
.. 语法被称为结构体更新语法:
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}
如上例子所示,除了 email 字段外,user2 希望使用 user1 的其他所有字段,这时就可以使用 .. 结构体更新语法。
let user2 = User {
email: String::from("another@example.com"),
..user1
};
非常简洁,不是吗?