跳转到内容

Rust 常用 trait

在 trait 那一页,你写过自己的 trait。其实标准库里早就有一组 trait,几乎每个 Rust 程序都会用到。它们回答的是关于类型的一些简单问题:能打印吗?能复制吗?能比较吗?能造一个“空”的出来吗?

可以把它们想象成制服上的徽章。每枚徽章都在说:“这个类型会做 X。”很多徽章用 #[derive(...)] 就能免费拿到,另一些需要你亲手写。这一页会带你认识最常见的几个,最后还有一张表,方便你以后回来查。

Debug 让你可以用 {:?} 打印一个值。它是给你这个开发者看的:写代码、修 bug 的时候拿来检查值。你几乎从来不用手写它,直接 derive 就好。用 {:#?} 可以得到“美化”版,每个字段占一行,大结构体读起来更轻松:

#[derive(Debug)]
struct Book {
title: String,
pages: u32,
}
fn main() {
let book = Book { title: String::from("Dune"), pages: 412 };
println!("{:?}", book);
println!("{:#?}", book);
}

输出:

Book { title: "Dune", pages: 412 }
Book {
title: "Dune",
pages: 412,
}
▶ 动手试一试

你在别人的代码里可能见过 dbg! 宏,它用的也是 Debug。

{} 用的是 Display。它负责面向用户的文字,也就是人在屏幕上读到的信息。这种文字该长什么样,Rust 猜不出来,所以 Display 没法 derive。你要自己实现 fmt::Display:

use std::fmt;
struct Book {
title: String,
author: String,
}
impl fmt::Display for Book {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} by {}", self.title, self.author)
}
}
fn main() {
let book = Book {
title: String::from("Dune"),
author: String::from("Frank Herbert"),
};
println!("{}", book);
let text = book.to_string();
println!("这段文字的长度是 {}", text.len());
}

输出:

Dune by Frank Herbert
这段文字的长度是 21
▶ 动手试一试

write! 的用法和 println! 一样,只是它写进格式化器 f,而不是屏幕。这里的 "{} by {}" 保留了英文:.len() 数的是字节,中文字符每个占 3 个字节,换成中文结果就不是 21 了。还有个不错的附赠:一个类型有了 Display,就自动拥有 .to_string()。在自定义错误那一页你还会再见到 Display,它负责给错误配上友好的提示信息。

两者都能复制出一份值,但方式不同。

  • Clone 给你一个 .clone() 方法。你得主动调用它,而且它可能真的要干点活,比如把 String 里的文字全部复制一遍。
  • Copy 会在每次赋值或传参时自动复制。只有能按位复制的小而简单的类型才允许实现它:数字、bool、char,以及只由这些组成的结构体。
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
#[derive(Debug, Clone)]
struct User {
name: String,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // Copy:p1 仍然可以使用
println!("{:?} 和 {:?}", p1, p2);
let u1 = User { name: String::from("Ana") };
let u2 = u1.clone(); // Clone:我们主动要求复制一份
println!("{:?} 和 {:?}", u1, u2);
}

输出:

Point { x: 1, y: 2 } 和 Point { x: 1, y: 2 }
User { name: "Ana" } 和 User { name: "Ana" }
▶ 动手试一试

如果没有 Copy,let p2 = p1; 会把 p1 移动走,之后你就不能再用它了(见所有权)。注意,Copy 总是要求同时实现 Clone,所以两个要一起 derive。

PartialEq 让你可以使用 == 和 !=。derive 出来的版本里,只有所有字段都相等,两个值才相等:

#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = Point { x: 1, y: 2 };
let c = Point { x: 5, y: 0 };
println!("a == b: {}", a == b);
println!("a != c: {}", a != c);
}

输出:

a == b: true
a != c: true
▶ 动手试一试

那 Eq 又是什么?它不添加任何新方法,只是一个承诺:“每个值永远等于它自己。”这听起来理所当然,但浮点数就做不到:在 Rust 里,f64::NAN == f64::NAN 的结果是 false。所以 f64 有 PartialEq,却没有 Eq。想把你的类型用作 HashMap 的键,或者放进 HashSet,就需要 Eq(再加上 Hash)。如果结构体里没有浮点数字段,两个都 derive 就行。

PartialOrd 让你可以使用 <、>、<= 和 >=。Ord 在此基础上提供完整的排序,.sort()、.max() 和 .min() 需要的就是它。derive 出来的版本会从上到下比较字段:先比第一个字段,只有相等时才看下一个。

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Version {
major: u32,
minor: u32,
patch: u32,
}
fn main() {
let old = Version { major: 1, minor: 2, patch: 0 };
let new = Version { major: 1, minor: 10, patch: 3 };
println!("old < new: {}", old < new);
let mut versions = vec![
Version { major: 2, minor: 0, patch: 0 },
new,
old,
];
versions.sort();
for v in &versions {
println!("{}.{}.{}", v.major, v.minor, v.patch);
}
}

输出:

old < new: true
1.2.0
1.10.3
2.0.0
▶ 动手试一试

因为字段是按顺序比较的,所以字段的书写顺序很重要。这里 major 最重要,这正好符合版本号的规则。对枚举来说,写在前面的变体算“更小”。和 Eq 一样,浮点数只有 PartialOrd,没有 Ord。想给浮点数排序的话,排序那一页有办法。

Default 给你的类型提供一个 ::default() 函数,用来造出一个“初始”值。derive 出来的版本会给每个字段用它自己的默认值:数字是 0,bool 是 false,还有空的 String 和空的 Vec。..Default::default() 语法让你只设置几个字段,其余的自动补齐:

#[derive(Debug, Default)]
struct Settings {
volume: u32,
muted: bool,
username: String,
}
fn main() {
let basic = Settings::default();
println!("{:?}", basic);
let custom = Settings {
volume: 80,
..Default::default()
};
println!("{:?}", custom);
}

输出:

Settings { volume: 0, muted: false, username: "" }
Settings { volume: 80, muted: false, username: "" }
▶ 动手试一试

如果你想要别的初始值(比如音量默认 50),就别 derive 了,自己写 impl Default for Settings,在 fn default() -> Self 里返回你想要的值。

From 描述怎样用一个类型构造出另一个类型。其实你已经用过它了:String::from("hi")。实现了 From 之后,Rust 会免费送你反方向的 Into:

struct Celsius(f64);
struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
fn main() {
let boiling = Fahrenheit::from(Celsius(100.0));
println!("水的沸点是 {}°F", boiling.0);
let body: Fahrenheit = Celsius(37.0).into();
println!("人的体温是 {:.1}°F", body.0);
}

输出:

水的沸点是 212°F
人的体温是 98.6°F
▶ 动手试一试

用 .into() 时,Rust 需要知道目标类型,所以我们写了 let body: Fahrenheit。

? 是怎样用到 From 的:这正是 From 大显身手的地方。? 运算符遇到错误时,会调用 From::from,把这个错误转换成函数返回的错误类型。所以只要给你自己的错误类型实现 From,? 就会替你转换错误:

use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
BadNumber(ParseIntError),
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::BadNumber(e)
}
}
fn parse_age(text: &str) -> Result<u32, AppError> {
let age: u32 = text.parse()?; // ParseIntError 在这里变成 AppError
Ok(age)
}
fn main() {
println!("{:?}", parse_age("42"));
println!("{:?}", parse_age("abc"));
}

输出:

Ok(42)
Err(BadNumber(ParseIntError { kind: InvalidDigit }))
▶ 动手试一试

自定义错误那一页会在这个思路上讲得更细。

在 Rust 里,a + b 其实是在调用 std::ops::Add trait 的 add 方法。给你的类型实现它,+ 就能直接用了。type Output 说明相加的结果是什么类型:

use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point { x: self.x + other.x, y: self.y + other.y }
}
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = Point { x: 3, y: 4 };
println!("{:?}", a + b);
}

输出:

Point { x: 4, y: 6 }
▶ 动手试一试

同样的思路也适用于 Sub(-)、Mul(*)、Neg(一元的 -)和 AddAssign(+=)。只在含义一目了然的时候这样做,比如两个点相加、两笔钱相加。如果读代码的人得猜 + 对你的类型意味着什么,那还不如写一个名字清楚的普通方法。

下面是一个普通结构体的快速参考:

trait 要 derive 吗? 能得到什么
Debug 几乎总是 {:?} 和 {:#?} 打印
Clone 通常要 .clone()
Copy 只用于没有 String、Vec 或 Box 字段的小类型 自动复制,而不是移动
PartialEq 通常要 == 和 !=
Eq 没有浮点数字段时 用作 HashMap 的键时需要(还要加上 Hash)
Hash 用作 HashMap 的键或放进 HashSet 时 计算哈希值
PartialOrd、Ord 值有自然顺序时 <、>、.sort()、.max()
Default “空”值有意义时 ::default() 和 ..Default::default()
Display 不能 derive,只能手写 {} 打印和 .to_string()

很常见的起手式是 #[derive(Debug, Clone, PartialEq)]。需要更多时再加,少了哪个,编译器会告诉你。

给含有 String 字段的结构体 derive Copy

#[derive(Clone, Copy)]
struct User {
name: String,
age: u32,
}
error[E0204]: the trait `Copy` cannot be implemented for this type
--> src/main.rs:2:8
|
1 | #[derive(Clone, Copy)]
| ---- in this derive macro expansion
2 | struct User {
| ^^^^
3 | name: String,
| ------------ this field does not implement `Copy`

编译器说:这个类型不能实现 Copy,因为 name 字段没有实现 Copy。String 拥有堆上的内存,没法按位复制。去掉 Copy,需要副本时用 .clone() 就好。

给含有浮点数的结构体 derive Eq

#[derive(Debug, PartialEq, Eq)]
struct Product { name: String, price: f64 }
error[E0277]: the trait bound `f64: Eq` is not satisfied
--> src/main.rs:2:32
|
1 | #[derive(Debug, PartialEq, Eq)]
| -- in this derive macro expansion
2 | struct Product { name: String, price: f64 }
| ^^^^^^^^^^ the trait `Eq` is not implemented for `f64`

编译器说:f64 没有实现 Eq。保留 PartialEq,去掉 Eq 即可。或者把价格存成以“分”为单位的 u64 整数,这样处理钱也更安全。

写一个 Task 结构体,包含 priority: u8 和 title: String。derive 需要的 trait,让你能给 Vec<Task> 排序、用 {:?} 打印每个任务,并用 == 比较两个任务。数字越小越紧急,应该排在前面。提示:.sort() 需要 Ord,而 Ord 还需要 Eq、PartialEq 和 PartialOrd。把 priority 写在最前面,这样它会最先参与比较。

查看答案
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Task {
priority: u8,
title: String,
}
fn main() {
let mut tasks = vec![
Task { priority: 3, title: String::from("给植物浇水") },
Task { priority: 1, title: String::from("交房租") },
Task { priority: 2, title: String::from("回复 Sam") },
];
let copy = tasks[1].clone();
println!("是同一个任务吗?{}", copy == tasks[1]);
tasks.sort();
for task in &tasks {
println!("{:?}", task);
}
}

输出:

是同一个任务吗?true
Task { priority: 1, title: "交房租" }
Task { priority: 2, title: "回复 Sam" }
Task { priority: 3, title: "给植物浇水" }
▶ 动手试一试

写一个 Money 结构体,用 cents: u64 存储以“分”为单位的整数金额。实现 From<u64>,让 350.into() 能用;实现 Display,让它打印成 $3.50 这样;再实现 Add,让两笔钱能用 + 相加。提示:美元数是 cents / 100,剩下的是 cents % 100。用 {:02} 打印剩下的部分,这样 5 分会显示成 05。derive Copy,这样相加之后还能继续打印原来的金额。

查看答案
use std::fmt;
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Money {
cents: u64,
}
impl From<u64> for Money {
fn from(cents: u64) -> Self {
Money { cents }
}
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "${}.{:02}", self.cents / 100, self.cents % 100)
}
}
impl Add for Money {
type Output = Money;
fn add(self, other: Money) -> Money {
Money { cents: self.cents + other.cents }
}
}
fn main() {
let coffee: Money = 350.into();
let cake = Money::from(405);
let total = coffee + cake;
println!("咖啡:{coffee}");
println!("蛋糕:{cake}");
println!("合计:{total}");
}

输出:

咖啡:$3.50
蛋糕:$4.05
合计:$7.55
▶ 动手试一试
  • Debug({:?})给开发者看,几乎总是 derive 出来的。Display({})给用户看,需要手写。
  • Clone 在你调用 .clone() 时才复制。Copy 会自动复制,但只适用于简单类型。
  • PartialEq/Eq 让你能用 ==。PartialOrd/Ord 让你能用 < 和排序,比较时从上到下逐个字段比较。
  • Default 构造一个初始值;..Default::default() 帮你补齐没设置的字段。
  • 实现 From,就免费得到 Into。? 运算符用 From 来转换错误。
  • 实现 std::ops::Add(以及它的同伴们),就能让 + 用在你自己的类型上。

想更进一步?高级 trait 讲解像 type Output 这样的关联类型、默认类型参数,以及怎样给不是你写的类型实现 trait。

小测验

1. println! 里的 {} 用的是哪个 trait?

2. 为什么含有 String 字段的结构体不能 derive Copy?

3. derive 了 PartialOrd 之后,两个结构体是怎样比较的?

4. 你实现了 From<ParseIntError> for AppError。这让 ? 能做什么?

5. 要让 a + b 能用在你自己的类型上,需要实现哪个 trait?

得分: 0 / 5