讲解
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)天天见。下一章攻克最后一个硬核概念:生命周期。