Rust 常用 trait
在 trait 那一页,你写过自己的 trait。其实标准库里早就有一组 trait,几乎每个 Rust 程序都会用到。它们回答的是关于类型的一些简单问题:能打印吗?能复制吗?能比较吗?能造一个“空”的出来吗?
可以把它们想象成制服上的徽章。每枚徽章都在说:“这个类型会做 X。”很多徽章用 #[derive(...)] 就能免费拿到,另一些需要你亲手写。这一页会带你认识最常见的几个,最后还有一张表,方便你以后回来查。
Debug:给开发者看的输出
Section titled “Debug:给开发者看的输出”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:给用户看的输出
Section titled “Display:给用户看的输出”{} 用的是 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这段文字的长度是 21write! 的用法和 println! 一样,只是它写进格式化器 f,而不是屏幕。这里的 "{} by {}" 保留了英文:.len() 数的是字节,中文字符每个占 3 个字节,换成中文结果就不是 21 了。还有个不错的附赠:一个类型有了 Display,就自动拥有 .to_string()。在自定义错误那一页你还会再见到 Display,它负责给错误配上友好的提示信息。
Clone 与 Copy
Section titled “Clone 与 Copy”两者都能复制出一份值,但方式不同。
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 与 Eq:用 == 比较
Section titled “PartialEq 与 Eq:用 == 比较”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: truea != c: true那 Eq 又是什么?它不添加任何新方法,只是一个承诺:“每个值永远等于它自己。”这听起来理所当然,但浮点数就做不到:在 Rust 里,f64::NAN == f64::NAN 的结果是 false。所以 f64 有 PartialEq,却没有 Eq。想把你的类型用作 HashMap 的键,或者放进 HashSet,就需要 Eq(再加上 Hash)。如果结构体里没有浮点数字段,两个都 derive 就行。
PartialOrd 与 Ord:用 < 和 > 比较
Section titled “PartialOrd 与 Ord:用 < 和 > 比较”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: true1.2.01.10.32.0.0因为字段是按顺序比较的,所以字段的书写顺序很重要。这里 major 最重要,这正好符合版本号的规则。对枚举来说,写在前面的变体算“更小”。和 Eq 一样,浮点数只有 PartialOrd,没有 Ord。想给浮点数排序的话,排序那一页有办法。
Default:合理的初始值
Section titled “Default:合理的初始值”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 与 Into:类型之间的转换
Section titled “From 与 Into:类型之间的转换”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 }))自定义错误那一页会在这个思路上讲得更细。
用 Add 重载运算符
Section titled “用 Add 重载运算符”在 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(+=)。只在含义一目了然的时候这样做,比如两个点相加、两笔钱相加。如果读代码的人得猜 + 对你的类型意味着什么,那还不如写一个名字清楚的普通方法。
应该 derive 哪些 trait?
Section titled “应该 derive 哪些 trait?”下面是一个普通结构体的快速参考:
| 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 expansion2 | 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 expansion2 | struct Product { name: String, price: f64 } | ^^^^^^^^^^ the trait `Eq` is not implemented for `f64`编译器说:f64 没有实现 Eq。保留 PartialEq,去掉 Eq 即可。或者把价格存成以“分”为单位的 u64 整数,这样处理钱也更安全。
1. 给待办清单排序
Section titled “1. 给待办清单排序”写一个 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); }}输出:
是同一个任务吗?trueTask { priority: 1, title: "交房租" }Task { priority: 2, title: "回复 Sam" }Task { priority: 3, title: "给植物浇水" }2. 能打印、能相加的金额
Section titled “2. 能打印、能相加的金额”写一个 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.55Debug({:?})给开发者看,几乎总是 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
- 可派生的 trait —《Rust 程序设计语言》附录 C:所有能
#[derive]的 trait,derive 出来的版本做了什么,以及什么时候需要它。 std::fmt— 格式化与 Display(英文):所有格式化选项,比如宽度、填充和{:#?},以及Display和Debug在其中的位置。- From 和 Into —《通过例子学 Rust》:在自定义类型之间转换的简短可运行示例。
std::ops— 运算符 trait(英文):所有可以重载的运算符,从Add到Index。std::cmp— 比较 trait(英文):详细介绍PartialEq、Eq、PartialOrd和Ord,以及每个 trait 承诺遵守的规则。