Rust Trait详解:定义共享行为与抽象接口

《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 selfself

  • 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>共享对象

  • 支付系统、插件系统与通知系统实战

相关推荐
再吃一根胡萝卜1 小时前
分布式事务:从“强一致”到“最终一致”,我为什么在微服务里放弃了 2PC?
后端
再吃一根胡萝卜1 小时前
从 Django 到 Spring Cloud:一个全栈开发者的微服务思考
后端
程序员爱钓鱼1 小时前
Go 编程实战:切片 Slice——灵活的动态数据集合
后端·面试·go
再吃一根胡萝卜8 小时前
微服务治理的“四大护法”:从 Django 视角理解 Spring Cloud 核心组件
后端
再吃一根胡萝卜8 小时前
面试终极挑战:如何用 Django 经验,回答 Java 微服务实战问题?
后端
To_OC9 小时前
装完 ESLint 它一声不吭?我还以为代码写得多好
后端·node.js·eslint
凤山老林10 小时前
精细化流量治理:Spring Boot 动态特性开关与灰度发布体系
java·spring boot·后端
vipbic11 小时前
一个前端的 9 天重构:我是怎么用 Codex 重做航栈的
前端·javascript·后端
用户9385156350711 小时前
TypeScript 高级类型 + CSS 三列布局:从类型体操到样式工程的进阶之路
css·面试·typescript