Leapcell 2025-03-30
概述
Rust 中的 trait(特性)和 trait bounds(特性约束)用于实现抽象和泛型约束。
Rust 中的 trait 类似于其他编程语言中常说的“接口”(interface),尽管存在一些差异。trait 告诉 Rust 编译器:某个特定类型具备某种功能,这种功能可以与其他类型共享。trait 允许我们以抽象的方式定义共享行为。我们可以使用 trait bounds 来指定泛型类型必须实现某些行为。
简单来说,trait 就像是 Rust 中的接口,它定义了类型在实现该 trait 时必须提供的行为。trait 可以约束多个类型之间共享的行为;当用于泛型编程时,它可以将泛型限制为符合该 trait 所指定行为的类型。
定义一个 Trait
如果不同的类型表现出相同的行为,我们可以定义一个 trait,然后为这些类型分别实现它。定义 trait 意味着将一组方法组合在一起,目的是描述某种行为以及实现该目的所需的一组要求。
trait 是一种接口,它定义了一系列方法:
pub trait Summary {
// trait 中的方法只需声明
fn summarize_author(&self) -> String;
// 此方法具有默认实现;其他类型无需自行实现
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
上述代码定义了一个名为 Summary 的 trait,它提供了两个方法:summarize_author 和 summarize。
- trait 中的方法只需声明,其实现由具体类型提供。
- 但方法也可以包含默认实现。此处
summarize方法有一个默认实现,它内部调用了没有默认实现的summarize_author方法。 Summarytrait 的两个方法都以self作为参数,就像结构体上的方法一样。这里的self是 trait 方法的第一个参数。
注意:实际上,
self是self: Self的简写,&self是self: &Self的简写,&mut self是self: &mut Self的简写。其中Self指代实现该 trait 的类型。例如,如果类型Foo实现了Summarytrait,那么在其实现内部,Self就指代Foo。
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("@{} posted a tweet...", self.summarize_author())
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
pub struct Post {
pub title: String,
pub author: String,
pub content: String,
}
impl Summary for Post {
fn summarize_author(&self) -> String {
format!("{} posted an article", self.author)
}
fn summarize(&self) -> String {
format!("{} posted: {}", self.author, self.content)
}
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("{} tweeted", self.username)
}
fn summarize(&self) -> String {
format!("@{} tweeted: {}", self.username, self.content)
}
}
fn main() {
let tweet = Tweet {
username: String::from("haha"),
content: String::from("the content"),
reply: false,
retweet: false,
};
println!("{}", tweet.summarize())
}
这里有一个关于 trait 和其实现在何处定义的重要原则,称为 孤儿规则(orphan rule):如果你想为类型 A 实现 trait T,那么 T 或 A 中至少有一个必须在当前 crate 中定义。
该规则确保他人编写的代码不会破坏你的代码,同样,你的代码也不会无意中破坏他人的代码。
将 Trait 用作函数参数
trait 可以用作函数参数。以下是一个使用 trait 作为参数的函数定义示例:
pub fn notify(item: &impl Summary) { // trait 参数
println!("Breaking news! {}", item.summarize());
}
参数 item 表示“一个实现了 Summary trait 的值”。你可以将任何实现了 Summary trait 的类型作为该函数的参数。在函数体内,也可以在该参数上调用 trait 中定义的方法。
Trait Bounds(特性约束)
上面使用的 impl Trait 语法实际上是语法糖。完整的语法如下:T: Summary,这被称为 trait bound(特性约束)。
pub fn notify<T: Summary>(item: &T) { // trait bound
println!("Breaking news! {}", item.summarize());
}
对于更复杂的使用场景,trait bounds 提供了更强的灵活性和表达能力。例如,一个函数接收两个都实现了 Summary trait 的参数:
pub fn notify(item1: &impl Summary, item2: &impl Summary) {} // trait 参数形式
pub fn notify<T: Summary>(item1: &T, item2: &T) {} // 泛型 T 的约束:要求 item1 和 item2 是同一类型,且该类型实现了 Summary trait
使用 + 指定多个 Trait Bound
除了单一约束外,你还可以指定多个约束,例如要求一个参数同时实现多个 trait:
pub fn notify(item: &(impl Summary + Display)) {} // 语法糖
pub fn notify<T: Summary + Display>(item: &T) {} // 完整的 trait bound 语法
使用 where 简化 Trait Bounds
当存在大量 trait 约束时,函数签名可能会变得难以阅读。此时可以使用 where 子句来简化语法:
// 当多个泛型类型具有许多 trait bound 时,签名可能难以阅读
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 { ... }
// 使用 `where` 进行简化,使函数名、参数和返回类型更靠近
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug {
...
}
使用 Trait Bounds 条件性地实现方法或 Trait
使用 trait bounds 作为参数,可以基于特定类型和 trait 条件性地实现方法,从而使函数能够接受多种类型的参数。例如:
fn notify(summary: impl Summary) {
println!("notify: {}", summary.summarize())
}
fn notify_all(summaries: Vec<impl Summary>) {
for summary in summaries {
println!("notify: {}", summary.summarize())
}
}
fn main() {
let tweet = Weibo {
username: String::from("haha"),
content: String::from("the content"),
reply: false,
retweet: false,
};
let tweets = vec![tweet];
notify_all(tweets);
}
函数中的 summary 参数使用了 impl Summary 而不是具体类型。这意味着该函数可以接受任何实现了 Summary trait 的类型。
当你希望拥有一个值,并且只关心它实现了某个特定 trait(而不关心其具体类型)时,可以使用 trait object(特性对象) 形式,即结合智能指针(如 Box)与关键字 dyn:
fn notify(summary: Box<dyn Summary>) {
println!("notify: {}", summary.summarize())
}
fn notify_all(summaries: Vec<Box<dyn Summary>>) {
for summary in summaries {
println!("notify: {}", summary.summarize())
}
}
fn main() {
let tweet = Tweet {
username: String::from("haha"),
content: String::from("the content"),
reply: false,
retweet: false,
};
let tweets: Vec<Box<dyn Summary>> = vec![Box::new(tweet)];
notify_all(tweets);
}
在泛型中使用 Trait
让我们看看 trait 如何在泛型编程中用于约束泛型类型。
在前面的例子中,我们将 notify 函数定义为 fn notify(summary: impl Summary),这表示 summary 参数的类型应实现 Summary trait,而不是指定一个具体类型。实际上,impl Summary 是泛型编程中 trait bound 的语法糖。使用 impl Trait 的代码可以重写为:
fn notify<T: Summary>(summary: T) {
println!("notify: {}", summary.summarize())
}
fn notify_all<T: Summary>(summaries: Vec<T>) {
for summary in summaries {
println!("notify: {}", summary.summarize())
}
}
fn main() {
let tweet = Tweet {
username: String::from("haha"),
content: String::from("the content"),
reply: false,
retweet: false,
};
let tweets = vec![tweet];
notify_all(tweets);
}
从函数返回 impl Trait
你可以使用 impl Trait 来指定函数返回一个实现了特定 trait 的类型:
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("haha"),
content: String::from("the content"),
reply: false,
retweet: false,
}
}
这种带有 impl Trait 的返回类型必须解析为单一的具体类型。如果函数可能返回实现同一 trait 的不同类型,则会导致编译错误。例如:
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
Tweet { ... } // 不能在此处返回两种不同类型
} else {
Post { ... } // 不能在此处返回两种不同类型
}
}
上述代码会报错,因为它返回了两种不同的类型——Tweet 和 Post,即使它们都实现了相同的 trait。如果你希望返回不同类型,必须使用 trait object:
fn returns_summarizable(switch: bool) -> Box<dyn Summary> {
if switch {
Box::new(Tweet { ... }) // trait object
} else {
Box::new(Post { ... }) // trait object
}
}
总结
Rust 的核心设计目标之一是 零成本抽象(zero-cost abstractions) —— 即允许使用高级语言特性,而不会牺牲运行时性能。这种零成本抽象的基础正是 泛型 和 trait。它们使得高级语法能够在编译期间被编译为高效的底层代码,从而实现运行时的高效性。
- Trait 以抽象方式定义共享行为。
- Trait bounds 定义对函数参数或返回类型的约束,例如
impl SuperTrait或T: SuperTrait。
Trait 和 trait bounds 使我们能够通过使用泛型类型参数来减少重复代码,同时仍能向编译器清晰地说明这些泛型类型必须实现哪些行为。因为我们向编译器提供了 trait bound 信息,所以它能够检查我们代码中实际使用的类型是否提供了正确的行为。
总而言之,Rust 中的 trait 主要有两个用途:
- 行为抽象:类似于接口,通过定义共享功能来抽象不同类型之间的共同行为。
- 类型约束:约束类型的行为,根据类型所实现的 trait 来缩小其可能的范围。