Rust Trait 特征

Trait 是 Rust 实现抽象和多态的方式,概念上类似 Java 的接口、Swift 的协议、Haskell 的 typeclass。但 Rust 的 trait 有自己的特色——尤其是和泛型、所有权结合后,能写出非常强大且零开销的抽象。这一篇我们讲透 trait 的核心用法。

1. Trait 是什么?

Trait 定义一组方法签名(可有默认实现),类型通过 impl Trait for Type 来"实现"它。这是一种抽象——你定义"能做什么",而不是"是什么":

// Trait 类似其他语言的"接口"(interface)
// 定义一组方法签名,类型通过 impl 来"实现"它

// 1. 定义一个 trait
pub trait Summary {
    fn summarize(&self) -> String;

    // 默认方法:有实现,实现者可以不重写
    fn describe(&self) -> String {
        String::from("(没有描述)")
    }
}

// 2. 定义结构体
pub struct NewsArticle {
    pub title: String,
    pub author: String,
    pub content: String,
}

pub struct Tweet {
    pub username: String,
    pub text: String,
}

// 3. 为 NewsArticle 实现 Summary
impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {}", self.title, self.author)
    }
    // describe 用默认实现,不重写也可以
}

// 4. 为 Tweet 实现 Summary(重写 summarize 和 describe)
impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("@{}: {}", self.username, self.text)
    }

    fn describe(&self) -> String {
        String::from("一条推特")
    }
}

fn main() {
    let article = NewsArticle {
        title: "Rust 1.0 发布".to_string(),
        author: "Mozilla".to_string(),
        content: "...".to_string(),
    };
    println!("{}", article.summarize());    // Rust 1.0 发布, by Mozilla
    println!("{}", article.describe());     // (没有描述)  默认方法

    let tweet = Tweet {
        username: "user".to_string(),
        text: "hello".to_string(),
    };
    println!("{}", tweet.describe());       // 一条推特
}

关键概念:

2. 泛型 + Trait Bound

trait 最强大的用法是作为泛型约束(trait bound)。让你写"接受任何实现了某 trait的类型"的函数:

// 泛型 + trait bound:写"能处理任何实现了某 trait 的类型"的函数

fn print_summary<T: Summary>(item: &T) {
    println!("{}", item.summarize());
}

// 简写:impl Trait 语法(更简洁,常见于函数参数)
fn print_summary2(item: &impl Summary) {
    println!("{}", item.summarize());
}

// 多个 trait bound 用 + 连接
fn show<T: Summary + std::fmt::Display>(item: &T) {
    println!("{} -> {}", item, item.summarize());
}

// where 子句:trait bound 复杂时更清晰
fn complex<T, U>(t: &T, u: &U) -> String
where
    T: Summary + Clone,
    U: std::fmt::Debug,
{
    format!("处理完成")
}

fn main() {
    let tweet = Tweet {
        username: "u".to_string(),
        text: "hi".to_string(),
    };
    print_summary(&tweet);
    print_summary2(&tweet);
}

// 返回 trait 类型也可以用 impl Trait
fn make_summary() -> impl Summary {
    Tweet {
        username: "auto".to_string(),
        text: "生成的".to_string(),
    }
}
// 注意:impl Trait 返回只能返回【单一具体类型】,
// 不能在分支返回不同类型(那种情况用 trait object)

几种写法:

重要原理:单态化(monomorphization)。泛型在编译期为每种具体类型生成一份代码,所以运行时零开销——和手写具体类型的代码一样快。这是 Rust"零成本抽象"的典范。

3. Trait Object(动态分发)

有时你需要在同一容器里混合多种类型(都实现同一 trait)。这时泛型不够用——泛型要求编译期就知道具体类型。Rust 提供 trait object:

// trait object(动态分发):用 dyn 关键字
// 可以在运行时持有"任何实现了该 trait 的类型"

fn main() {
    // Vec<Box<dyn Summary>> 能装不同类型(都实现了 Summary)
    let items: Vec<Box<dyn Summary>> = vec![
        Box::new(NewsArticle {
            title: "新闻".to_string(),
            author: "记者".to_string(),
            content: "...".to_string(),
        }),
        Box::new(Tweet {
            username: "user".to_string(),
            text: "推文".to_string(),
        }),
    ];

    for item in &items {
        println!("{}", item.summarize());
    }
}

// 静态分发(泛型) vs 动态分发(trait object):
// - 泛型 <T: Trait>:编译期单态化,每个具体类型生成一份代码,运行时零开销
// - dyn Trait:运行时通过虚表分发,有一点开销,但能装不同类型
//
// 选择:
// - 同一容器需要混合多种类型 -> 用 dyn
// - 性能敏感,类型在编译期可知 -> 用泛型

选择指南:

大多数场景用泛型更合适。只有当类型组合在编译期无法确定(如插件系统、异构集合),才用 dyn。

4. Derive:自动实现常用 Trait

Rust 提供了一堆常用 trait(Debug、Clone、PartialEq 等),手动写很啰嗦。#[derive] 让编译器自动生成:

// 常用 trait 大多可以通过 #[derive] 自动实现

#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = p1.clone();            // Clone 派生
    println!("{:?}", p1);           // Debug 派生 -> Point { x: 1, y: 2 }
    println!("{}", p1 == p2);       // PartialEq 派生 -> true
}

// 标准库常用 trait:
// Debug        — 调试输出 {:?}
// Clone, Copy  — 复制语义
// PartialEq    — 相等比较 ==
// PartialOrd   — 大小比较 < > <= >=
// Hash         — 可作 HashMap key
// Default      — 默认值 default()
// Serialize/Deserialize  — 需要 serde crate,序列化

// 派生的原理:编译器自动生成对应的 impl 块
// 你也可以手动实现这些 trait(复杂场景),或派生后手动覆盖

派生 trait 的原理是编译器为你自动生成对应的 impl 块。这是 Rust 减少样板代码的利器——大多数类型只要加一行 #[derive(Debug, Clone, PartialEq)],就自动获得这些能力。

第三方库 serde 提供 #[derive(Serialize, Deserialize)],让你的类型自动支持 JSON/YAML 等序列化。这是 Rust Web 开发的基石。

5. Display vs Debug

两个最常手动实现的 trait 是 Display(控制 {} 输出,面向用户)和 Debug(控制 {:?:} 输出,面向开发者):

use std::fmt;

struct Color {
    r: u8,
    g: u8,
    b: u8,
}

// 实现 Display trait:控制 println! 的 {} 输出
impl fmt::Display for Color {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "rgb({}, {}, {})", self.r, self.g, self.b)
    }
}

// 实现 Debug:控制 {:?} 输出(也可派生,这里手动写)
impl fmt::Debug for Color {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Color(#{:02x}{:02x}{:02x})", self.r, self.g, self.b)
    }
}

fn main() {
    let c = Color { r: 255, g: 128, b: 0 };
    println!("{}", c);      // rgb(255, 128, 0)     Display
    println!("{:?}", c);    // Color(#ff8000)        Debug
}

// Display 必须手动实现(不能 derive),
// 因为编译器不知道你想怎么格式化。

区别:

6. 孤儿规则(orphan rule)

Rust 有一个重要的规则:你只能为自己定义的类型自己定义的 trait实现 trait——至少有一个是"你的"。这避免了不同 crate 互相覆盖对方 trait 实现的混乱。

这个规则让 trait 实现不会冲突,保证了大型项目的可组合性。

7. Trait 与继承

Rust 没有传统的类继承(没有"子类继承父类字段和方法")。Trait 可以要求实现者同时实现另一个 trait(supertrait):

8. 关联类型(Associated Type)

Trait 可以有关联类型,用于"trait 内部用到的类型",比纯泛型更清晰。经典例子是 Iterator trait,有 type Item 表示迭代出的元素类型。实现 Iterator 时指定 Item 的具体类型。这让 trait 既灵活又类型安全。日常用标准库的迭代器时,你会接触到这个概念。

9. 实战:自定义 trait

实战中,trait 用于:

小结

这一篇你掌握了 Rust 抽象的核心工具 Trait:定义方法签名、为类型实现、默认方法、泛型 trait bound(零成本抽象)、impl Trait 语法糖、trait object(动态分发)、#[derive] 自动派生、Display/Debug 手动实现。Trait + 泛型是 Rust 复用代码的标准方式,替代了传统面向对象的继承。

下一篇我们看 错误处理——Rust 没有 try/catch,而是用 Result 类型和问号运算符,这是它"健壮性"的重要一环。

← 上一篇 Rust 枚举与模式匹配

下一篇 Rust 错误处理

✈️💬