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方法默认实现内部还调用了不包含默认实现的summarize_author方法。 - Summary trait的两个方法的参数中都包含关键字
self,与结构体方法一样,self用作 trait 方法的第一个参数。
注:实际上
self是self: Self的简写,&self是self: &Self的简写,&mut self是self &mut Self的简写。Self代表的是当前实现了 trait 的类型,例如有一个类型 Foo 实现了 Summary trait,则实现方法时中的 Self 就是Foo。
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("@{}发表了帖子...", 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!("{}发表了贴子", self.author)
}
fn summarize(&self) -> String {
format!("{}发表了贴子:{}", self.author, self.content)
}
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("{}发表了微博", self.username)
}
fn summarize(&self) -> String {
format!("@{}发表了微博:{}", 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 实现与定义的位置,有一条非常重要的原则(孤儿原则): 如果你想要为类型 A 实现trait T,那么 A 或者 T 至少有一个是在当前作用域中定义的!
这个规则可以确保其他人编写的代码不会破坏你的代码,也确保了你不会莫名其妙破坏别人的代码。
使用 trait 作为函数参数
Trait 可以用作函数参数,这里先定义一个函数,使用 trait 用作函数参数:
pub fn notify(item: &impl Summary) { // trait 参数
println!("Breaking news! {}", item.summarize());
}
参数的意思就是 实现了 Summary trait 的 item 参数。可以使用任何实现了 Summary trait 的类型作为该函数的参数,同时在函数体内,还可以调用该 trait 的方法。
Trait 约束 (trait bound)
上面使用的 impl Trait 实际上只是一个语法糖,其完整书写形式如下,形如 T: Summary 被称为 trait 约束。
pub fn notify<T: Summary> (item: &T) { // trait 约束
println!("Breaking news! {}", item.summarize());
}
对于比较复杂的使用场景,特征约束可以让我们拥有更大的灵活性和语法表现能力,例如一个函数接受两个 impl Summary 的参数:
pub fn notify(item1: &impl Summary, item2: &impl Summary) {} // trait 参数
pub fn notify<T: Summary>(item1: &T, item2: &T) {} // 泛型 T 约束:说明 item1 和 item2 必须拥有同样的类型,同时说明 T 必须实现 Summary trait
通过 + 指定多个 trait bound
除了单个约束条件,还可以指定多个约束条件,例如让参数实现多个 trait:
pub fn notify(item: &(impl Summary + Display)) {} // 语法糖形式
pub fn notify<T: Summary + Display> (item: &T) {} // trait bound 完整形式
通过 where 简化 trait bound
当特征约束变得很多时,函数的签名就会变得很复杂,这时如果使用 where 可以对其做一些形式上的改进:
// 当出现多个泛型类型时,过多的trait bound会导致函数签名难以阅读
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 { ... }
// 使用 where 做简化,使得函数名、参数列表和返回值类型都离得很近,看起来跟没有那么多 trait bounds 的函数很像
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug {
....
}
使用 trait 约束有条件地实现方法或 trait
trait 约束作为参数,可以让我们在指定类型+指定 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 而不是其具体类型的时候,可以使用智能指针 Box 和关键字 dyn 组合的 trait 对象(注:后续将单独章节学习 trait 对象)。
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 限制泛型类型的行为。
上面定义 trait 例子中 notify 函数fn notify(summary: impl Summary),对于 summary 参数类型,指定的是 impl 关键字加 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,如果多个类型的 trait 就会报错,比如:
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
Tweet { ... } // 此处不能返回两个不同的类型
} else {
Post { ... } // 此处不能返回两个不同的类型
}
}
上面的代码会报错,因为返回了两个不同的类型 trait: Tweet 和 Post。如果想要实现返回不同的类型,需要使用trait 对象,关于这部分后续将单独章节学习。
fn returns_summarizable(switch: bool) -> Box<dyn Summary> {
if switch {
Box::new(Tweet { ... }) // trait 对象
} else {
Box::new(Post { ... }) // trait 对象
}
}
总结
在 Rust 设计目标中,零成本抽象是非常重要的一条,它让 Rust 具备高级语言表达能力的同时,又不会带来性能损耗。零成本的基石是泛型与 trait,它们可以在编译期把高级语法编译成与高效的底层代码,从而实现运行时的高效。
trait 是以抽象的方式定义共享的行为,trait bound 是对函数参数或者返回值定义类型约束,比如impl SuperTrait 或 T: SuperTrait,trait 和 trait bound 让我们使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。因为我们向编译器提供了 trait bound 信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为。综上,trait 用途简单的概括为:
- 抽象行为:类似接口,是对类型共性的同一性抽象,定义共同行为
- 类型约束:类型的行为被 trait 限定在更有限的范围内