讲解

Trait(特征)定义一组共享行为,类似其他语言的接口,但用法更灵活。定义用 trait 关键字列出一组方法签名;某个类型用 impl Trait名 for 类型名 提供实现。方法可以有默认实现(trait 里直接写函数体),实现方可以沿用或覆盖——标准库里大量 trait 靠默认实现让你只需写一两个方法就获得十几个能力。

两条关键规则。孤儿规则(orphan rule):「为类型 X 实现 trait Y」这件事,X 和 Y 至少有一个是你自己 crate 里定义的——不能给别人的类型实现别人的 trait(比如给 i32 实现 Display),这条规则保证了不同库的实现不会互相打架。想绕过它,标准手法是定义一个自己的新类型包一层(newtype 模式)。

trait 作为参数有两种写法:impl Trait 简写(fn notify(item: &impl Summary),读作「任何实现了 Summary 的引用」)和泛型约束完整版(fn notify<T: Summary>(item: &T)),效果等价;多个约束用 + 连接(T: Summary + Clone)。调用 trait 方法要求该 trait 在当前作用域可见(被 use 引入)——标准库 trait 多数已在 prelude 里,第三方 trait 方法「找不到」时先检查有没有 use。

示例

定义 trait、默认实现、作为参数使用:

trait Summary {
    // 实现方必须提供
    fn title(&self) -> String;

    // 默认实现,可覆盖
    fn preview(&self) -> String {
        format!("【{}】", self.title())
    }
}

struct Article {
    headline: String,
    body: String,
}

impl Summary for Article {
    fn title(&self) -> String {
        self.headline.clone()
    }
}

struct Tweet {
    username: String,
}

impl Summary for Tweet {
    fn title(&self) -> String {
        format!("@{}", self.username)
    }

    // 覆盖默认实现
    fn preview(&self) -> String {
        format!("推文 {}", self.title())
    }
}

// 参数是「任何实现了 Summary 的类型」
fn print_preview(item: &impl Summary) {
    println!("{}", item.preview());
}

fn main() {
    let a = Article {
        headline: String::from("Rust 教程上线"),
        body: String::from("正文略"),
    };
    let t = Tweet {
        username: String::from("rustacean"),
    };
    print_preview(&a);
    print_preview(&t);
    assert_eq!(a.preview(), "【Rust 教程上线】");
    assert_eq!(t.preview(), "推文 @rustacean");
}

标准库 trait 实战:手写 Display,derive 拿 Debug:

use std::fmt;

#[derive(Debug)]
struct Celsius(f64);

impl fmt::Display for Celsius {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.1}°C", self.0)
    }
}

fn main() {
    let temp = Celsius(36.567);
    assert_eq!(format!("{temp}"), "36.6°C"); // {} 用 Display
    println!("调试视图:{temp:?}"); // {:?} 用 Debug
    assert_eq!(temp.to_string(), "36.6°C"); // Display 自动带来 to_string()
}

常见坑

  • 给外部类型实现外部 trait:impl Display for Vec 编译失败(孤儿规则);包一层自己的 struct Wrapper(Vec) 再实现。
  • trait 没 use 就调方法:报「no method named」时先检查 trait 是否在作用域——错误信息通常会提示你该 use 谁。
  • impl Trait 参数以为能传不同类型:fn f(a: &impl Summary, b: &impl Summary) 的 a 和 b 可以是不同具体类型,这没问题;但同一个参数位置每次调用只能是一种类型。
  • 忘记 derive:{:?}、==、clone() 分别来自 Debug/PartialEq/Clone,缺哪个编译器都会提示,多数结构体开头就是一排 derive。

小结

trait 定义共享行为,可带默认实现;孤儿规则限制跨界实现;impl Trait / <T: Trait> 让函数面向行为编程;标准库 trait(Display、Debug、Clone)天天见。下一章攻克最后一个硬核概念:生命周期。