《Rust编程实战》系列第45篇
上一篇文章中,我们学习了Rust的
Enum,了解了如何通过枚举表达有限状态以及不同类型的数据。本篇开始进入Rust类型系统中非常重要的一部分:
Trait
Trait通常可以理解为"特征""行为约束"或者"接口"。如果Struct解决的是:
一个类型拥有哪些数据。
那么Trait主要解决:
不同类型能够提供哪些共同的行为。
例如用户、商品、订单虽然是完全不同的Struct,但它们可能都需要:
-
输出名称
-
转换为字符串
-
校验数据
-
保存数据
-
记录日志
我们可以定义一个统一的Trait:
php
trait Printable {
fn print(&self);
}
然后让不同类型分别实现它:
rust
struct User {
name: String,
}
struct Product {
name: String,
}
impl Printable for User {
fn print(&self) {
println!("用户:{}", self.name);
}
}
impl Printable for Product {
fn print(&self) {
println!("商品:{}", self.name);
}
}
调用者只需要知道这些类型实现了Printable,而不用关心它们内部的数据结构。本文将介绍:
-
什么是Trait
-
Trait基本语法
-
为Struct实现Trait
-
Trait方法
-
默认实现
-
覆盖默认方法
-
Trait作为参数
-
impl Trait -
Trait Bound
-
where语法 -
多Trait约束
-
Trait与泛型
-
返回
impl Trait -
标准库Trait
-
Trait设计实战
什么是Trait
Trait用于定义多个类型可以共享的一组行为。
基本语法:
rust
trait Summary {
fn summary(&self) -> String;
}
这里定义了一个:
css
Summary
Trait,并要求所有实现它的类型必须提供:
css
summary()
方法。
Trait本身通常不负责存储数据,它主要描述:
这个类型必须能够做什么
因此可以把Trait理解成一种行为契约。
为Struct实现Trait
先定义User:
rust
struct User {
name: String,
age: u32,
}
定义Trait:
rust
trait Summary {
fn summary(&self) -> String;
}
实现:
rust
impl Summary for User {
fn summary(&self) -> String {
format!(
"{},{}岁",
self.name,
self.age
)
}
}
使用:
rust
fn main() {
let user = User {
name: String::from("Tom"),
age: 20,
};
println!("{}", user.summary());
}
关键语法:
rust
impl Trait名称 for 类型名称
例如:
rust
impl Summary for User
表示:
为User实现Summary这个Trait。
多个类型实现同一个Trait
Trait最大的价值之一,是让不同类型拥有统一行为。
例如:
rust
struct Product {
name: String,
price: f64,
}
impl Summary for Product {
fn summary(&self) -> String {
format!(
"{},价格:{}",
self.name,
self.price
)
}
}
现在:
sql
User
Product
虽然数据结构完全不同,但都实现:
css
Summary
所以都可以:
ini
user.summary();
product.summary();
这就是Rust中的行为抽象。
Trait方法可以有参数
Trait方法和普通方法一样,可以接收参数:
rust
trait Discount {
fn discount_price(
&self,
rate: f64,
) -> f64;
}
实现:
rust
struct Product {
price: f64,
}
impl Discount for Product {
fn discount_price(
&self,
rate: f64,
) -> f64 {
self.price * rate
}
}
调用:
ini
let product = Product {
price: 100.0,
};
println!(
"{}",
product.discount_price(0.8)
);
Trait方法可以修改对象
Trait方法也可以使用:
rust
&mut self
例如:
php
trait Enable {
fn enable(&mut self);
fn disable(&mut self);
}
实现:
rust
struct User {
enabled: bool,
}
impl Enable for User {
fn enable(&mut self) {
self.enabled = true;
}
fn disable(&mut self) {
self.enabled = false;
}
}
调用:
ini
let mut user = User {
enabled: false,
};
user.enable();
Trait方法也可以消费self
例如:
rust
trait IntoName {
fn into_name(self) -> String;
}
实现:
rust
struct User {
name: String,
}
impl IntoName for User {
fn into_name(self) -> String {
self.name
}
}
调用:
ini
let user = User {
name: String::from("Tom"),
};
let name = user.into_name();
调用后user被消费,因为方法取得了:
lua
self
所有权。
Trait默认实现
Trait不仅可以声明方法,还可以提供默认实现。
例如:
rust
trait Summary {
fn summary(&self) -> String {
String::from("暂无简介")
}
}
如果某个类型不需要自定义行为:
rust
struct Category;
impl Summary for Category {}
那么:
ini
let category = Category;
println!("{}", category.summary());
会直接使用Trait提供的默认实现。
覆盖默认实现
如果类型需要自己的行为,可以重新实现:
rust
struct User {
name: String,
}
impl Summary for User {
fn summary(&self) -> String {
format!("用户:{}", self.name)
}
}
此时调用:
scss
user.summary()
会使用User自己的实现,而不是默认方法。
默认方法调用必需方法
Trait默认方法可以调用Trait中其他方法。
例如:
rust
trait Summary {
fn title(&self) -> &str;
fn summary(&self) -> String {
format!("标题:{}", self.title())
}
}
实现时只需要提供:
scss
title()
例如:
rust
struct Article {
title: String,
}
impl Summary for Article {
fn title(&self) -> &str {
&self.title
}
}
summary()会自动使用默认实现。
这种设计可以减少重复代码。
Trait作为函数参数
假设我们只希望函数接收实现Summary的类型。
可以写:
rust
fn print_summary(
value: &impl Summary,
) {
println!("{}", value.summary());
}
调用:
scss
print_summary(&user);
print_summary(&product);
只要参数类型实现了:
css
Summary
就可以传入。
这种语法叫:
rust
impl Trait
impl Trait参数语法
例如:
rust
fn show(value: &impl Summary) {
println!("{}", value.summary());
}
可以理解为:
参数可以是任何实现了Summary Trait的具体类型。
这比为每一种类型分别写函数更加通用。
不需要:
scss
fn show_user(user: &User)
fn show_product(product: &Product)
统一使用:
php
fn show(value: &impl Summary)
即可。
Trait Bound
impl Trait是一种简洁写法。
更完整的泛型形式是:
less
fn show<T: Summary>(
value: &T,
) {
println!("{}", value.summary());
}
这里:
makefile
T: Summary
称为:
Trait Bound
Trait约束
意思是:
泛型类型T必须实现Summary。
如果某个类型没有实现Summary,就无法传入。
impl Trait与泛型写法
下面两个函数在很多简单场景下表达类似意图:
javascript
fn show(
value: &impl Summary,
) {
}
以及:
csharp
fn show<T: Summary>(
value: &T,
) {
}
简单参数可以优先使用:
css
impl Summary
当需要表达多个参数之间类型关系时,泛型更加灵活。
两个参数必须是同一类型
例如:
less
fn compare<T: Summary>(
first: &T,
second: &T,
) {
}
这里要求:
sql
first和second必须是同一个具体类型T
因此可以:
scss
compare(&user1, &user2);
但不能直接:
scss
compare(&user, &product);
即使User和Product都实现了Summary,因为它们不是同一个具体类型。
两个参数可以是不同类型
如果允许不同类型,可以:
less
fn compare<T, U>(
first: &T,
second: &U,
)
where
T: Summary,
U: Summary,
{
println!("{}", first.summary());
println!("{}", second.summary());
}
这样可以传:
scss
compare(&user, &product);
多个Trait约束
一个类型可以要求同时实现多个Trait。
例如:
php
trait Summary {
fn summary(&self) -> String;
}
trait Printable {
fn print(&self);
}
函数要求两者:
csharp
fn process<T>(
value: &T,
)
where
T: Summary + Printable,
{
println!("{}", value.summary());
value.print();
}
也可以简写:
csharp
fn process<T: Summary + Printable>(
value: &T,
) {
}
这里:
diff
+
表示必须同时实现多个Trait。
where语法
如果Trait约束很多:
csharp
fn process<T: Summary + Printable + Clone>(
value: T,
) {
}
签名会越来越长。
可以改成:
csharp
fn process<T>(
value: T,
)
where
T: Summary + Printable + Clone,
{
}
多个泛型:
less
fn compare<T, U>(
first: T,
second: U,
)
where
T: Summary + Clone,
U: Summary + Debug,
{
}
where在复杂泛型代码中通常更容易阅读。
返回impl Trait
impl Trait也可以用于返回值:
rust
fn create_user() -> impl Summary {
User {
name: String::from("Tom"),
}
}
调用者知道返回值实现了:
css
Summary
但不需要依赖具体类型。
例如:
ini
let value = create_user();
println!("{}", value.summary());
这种方式适合隐藏内部具体实现类型。
返回impl Trait的重要限制
下面这种代码通常不成立:
rust
fn create(
is_user: bool,
) -> impl Summary {
if is_user {
User {
name: String::from("Tom"),
}
} else {
Product {
name: String::from("Book"),
}
}
}
虽然User和Product都实现Summary,但:
css
impl Summary
返回值背后仍然必须是一个确定的具体类型。
不能一个分支返回User,另一个返回Product。
如果确实需要运行时返回不同类型,通常需要:
css
Trait Object
这正是下一篇要重点学习的内容。
Trait与普通impl方法区别
普通方法:
rust
impl User {
fn name(&self) -> &str {
&self.name
}
}
只属于User。
Trait方法:
rust
impl Summary for User {
fn summary(&self) -> String {
...
}
}
来自某个共同接口。
简单理解:
rust
impl User
实现User自己的功能
impl Trait for User
实现Trait要求的统一行为
一个类型实现多个Trait
例如:
rust
struct User {
name: String,
}
trait Summary {
fn summary(&self) -> String;
}
trait Printable {
fn print(&self);
}
impl Summary for User {
fn summary(&self) -> String {
self.name.clone()
}
}
impl Printable for User {
fn print(&self) {
println!("{}", self.name);
}
}
一个类型可以根据需要实现大量Trait。
多个Trait存在同名方法
假设:
php
trait A {
fn print(&self);
}
trait B {
fn print(&self);
}
User同时实现:
rust
impl A for User {
fn print(&self) {
println!("A");
}
}
impl B for User {
fn print(&self) {
println!("B");
}
}
调用时可能需要明确指定:
css
A::print(&user);
B::print(&user);
这种写法用于消除方法名称歧义。
Trait关联函数
Trait也可以定义没有self的关联函数:
rust
trait Create {
fn create() -> Self;
}
实现:
rust
struct User {
name: String,
}
impl Create for User {
fn create() -> Self {
Self {
name: String::from("Guest"),
}
}
}
调用:
ini
let user = User::create();
Trait与Debug
我们经常使用:
bash
#[derive(Debug)]
这里的:
rust
Debug
本身就是标准库Trait。
它让类型支持:
arduino
println!("{:?}", value);
例如:
rust
#[derive(Debug)]
struct User {
name: String,
}
相当于让编译器帮助我们实现Debug Trait。
Trait与Clone
同样:
bash
#[derive(Clone)]
表示实现:
rust
Clone
Trait。
之后可以:
ini
let second = first.clone();
所以之前学习的:
rust
Debug
Clone
Copy
其实全部都属于Trait。
Display Trait
如果希望自己的类型支持:
arduino
println!("{}", value);
可以实现:
c
std::fmt::Display
例如:
rust
use std::fmt;
struct User {
name: String,
}
impl fmt::Display for User {
fn fmt(
&self,
f: &mut fmt::Formatter<'_>,
) -> fmt::Result {
write!(f, "用户:{}", self.name)
}
}
然后:
rust
let user = User {
name: String::from("Tom"),
};
println!("{}", user);
这里:
css
Display
也是Trait。
Trait实现规则
Rust对Trait实现有一条重要限制,通常称为孤儿规则。
简单理解:
一个Trait实现中,Trait或者目标类型至少有一个必须属于当前Crate。
例如自己定义:
css
trait Summary {}
可以为标准库类型实现:
rust
impl Summary for String {
}
因为Trait是自己定义的。
也可以为自己的User实现标准库Trait:
rust
impl Display for User {
}
因为User属于自己。
但通常不能直接为两个都来自外部Crate的类型和Trait添加实现。
这样可以避免不同库之间出现实现冲突。
实战:支付Trait
假设系统支持不同支付方式,可以定义统一行为:
rust
trait Payment {
fn pay(
&self,
amount: f64,
) -> Result<(), String>;
}
银行卡:
rust
struct BankCard {
card_number: String,
}
impl Payment for BankCard {
fn pay(
&self,
amount: f64,
) -> Result<(), String> {
println!(
"银行卡支付:{},金额:{}",
self.card_number,
amount
);
Ok(())
}
}
余额支付:
rust
struct Balance {
user_id: u64,
}
impl Payment for Balance {
fn pay(
&self,
amount: f64,
) -> Result<(), String> {
println!(
"用户{}余额支付:{}",
self.user_id,
amount
);
Ok(())
}
}
通用函数:
rust
fn checkout(
payment: &impl Payment,
amount: f64,
) -> Result<(), String> {
payment.pay(amount)
}
现在不同支付类型都可以通过统一接口完成支付。
实战:通知系统
定义:
php
trait Notification {
fn send(&self, message: &str);
}
邮件:
rust
struct Email {
address: String,
}
impl Notification for Email {
fn send(&self, message: &str) {
println!(
"邮件发送到{}:{}",
self.address,
message
);
}
}
短信:
rust
struct Sms {
phone: String,
}
impl Notification for Sms {
fn send(&self, message: &str) {
println!(
"短信发送到{}:{}",
self.phone,
message
);
}
}
统一处理:
rust
fn notify(
sender: &impl Notification,
message: &str,
) {
sender.send(message);
}
调用者不需要关心具体是Email还是Sms,只需要关心:
它实现了Notification
Trait与静态分发
使用:
less
fn show<T: Summary>(value: &T)
或者:
php
fn show(value: &impl Summary)
时,编译器通常可以在编译阶段知道具体类型,并为具体类型生成对应代码。
这种方式通常称为:
scss
Static Dispatch
静态分发
它的特点通常是:
-
编译阶段确定具体类型
-
方法调用容易被优化
-
通常没有动态查找开销
但缺点是:
一个Vec中不能简单地同时保存多个不同具体类型的
impl Trait值。例如希望同时保存:
css
User
Product
Article
而它们都实现Summary,就需要下一篇的Trait Object。
Trait常见错误
定义Trait却没有实现方法
例如:
rust
trait Summary {
fn summary(&self) -> String;
}
impl Summary for User {
}
如果Trait没有默认实现,就必须实现要求的方法。
忘记导入Trait
某些Trait来自其他模块时,即使类型实现了Trait,如果Trait没有进入作用域,也可能无法直接调用对应方法。
通常需要:
rust
use crate::Summary;
把Trait当具体类型
错误思路:
php
fn show(value: Summary)
Trait本身不是普通具体类型。
通常使用:
css
impl Summary
泛型Trait Bound,或者Trait Object。
认为返回impl Trait可以返回多个不同类型
rust
fn create() -> impl Summary
通常必须返回同一个具体类型。
Trait约束过度
如果函数只需要:
css
Summary
不要无意义要求:
css
Summary + Clone + Debug + Display
约束越多,API越难使用。
Trait最佳实践
Trait表达行为而不是数据
推荐:
php
trait Save {
fn save(&self);
}
而不是试图让Trait保存字段。
Trait名称体现能力
常见风格:
rust
Display
Clone
Read
Write
Iterator
Serialize
一个好的Trait应该表达"这个类型能够做什么"。
简单参数优先impl Trait
例如:
php
fn show(value: &impl Summary)
清晰直接。
复杂类型关系使用泛型和where
例如:
less
fn process<T, U>(
first: T,
second: U,
)
where
T: Summary + Clone,
U: Summary,
{
}
Trait默认方法用于共享逻辑
如果多个实现拥有相同行为,可以把公共逻辑放入默认方法。
不要设计巨型Trait
一个Trait包含几十个无关方法,会让实现非常困难。
更推荐拆分:
Readable
Writable
Closable
等小型Trait。
根据需求选择静态或动态分发
泛型与impl Trait通常适合编译阶段已知类型。
当运行时需要保存多种不同类型时,则考虑Trait Object。
本章小结
Trait是Rust实现抽象和代码复用的核心机制之一。
本文学习了:
-
Trait用于定义共享行为
-
使用
trait声明行为接口 -
使用
impl Trait for Type实现Trait -
不同Struct可以实现同一个Trait
-
Trait方法支持
&self、&mut self和self -
Trait可以提供默认实现
-
类型可以覆盖默认方法
-
impl Trait可以作为函数参数 -
T: Trait称为Trait Bound -
多Trait约束使用
+ -
复杂约束可以使用
where -
impl Trait可以作为返回值 -
一个类型可以实现多个Trait
-
Debug、Clone、Copy、Display都是Trait
-
Trait实现受到孤儿规则约束
-
泛型Trait调用通常属于静态分发
-
Trait适合定义支付、通知等统一业务能力
可以记住:
Struct定义数据,Enum定义状态,Trait定义能力。
Trait并不关心一个类型内部有什么字段,它只关心这个类型能否提供约定的行为。
当多个不同类型拥有相同行为时,Trait就是Rust中最重要的抽象工具之一。
下一篇预告
下一篇我们将继续学习Trait体系中非常关键的内容:
Rust Trait Object详解:dyn Trait与动态分发
内容包括:
-
什么是Trait Object
-
dyn Trait -
&dyn Trait -
Box<dyn Trait> -
为什么不同类型不能直接放入同一个Vec
-
使用Trait Object保存不同类型
-
静态分发与动态分发
-
vtable虚函数表
-
Trait Object的内存结构
-
Object Safety
-
哪些Trait不能作为Trait Object
-
Box<dyn Trait>所有权 -
Arc<dyn Trait>共享对象 -
支付系统、插件系统与通知系统实战