前言#
昨天我们学了泛型generic
今天我们来学trait
traits:定义共享行为#
前面多次提到的trait,今天终于见上一面了。
trait定义了某个特定类型具有的功能,并且还能被其他类型分享。
有点拗口,实际上就是这个特定类型里的某一个方法,而这个方法还能被用于分享使用,那就是一个trait。
这么看,和其它语言中interface也就是接口挺像的。
定义一个trait#
一个类型的行为(behavior)是由这个类型所拥有的method组成的。通俗的说就是,你在大众的眼里是怎么样的取决于你所做的事情(你干嘛~哎呦)
我们先来看下如何定义
pub trait Summary {
fn summarize(&self) -> String;
}关键字是trait,用法和struct定义method很像。
我们定义了一个Summary的trait,以及它里面summarize的method。
trait也是可以pub出去的,这样可以被别的crate使用。
然后让我们来实现这个trait
为一个类型实现(implement)一个trait#
直接来看下例子
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}我们定义了一个新闻文章NewsArticle的struct,有标题、地址、作者以及内容四个属性。
然后我们给这个NewsArticle的struct实现了Summary这个trait,重新定义了summarize这个方法。
同样的,我们又声明了一个Tweet的struct,有用户名、内容、回复以及转发四个属性。
也给它实现了Summary这个trait,但是重写的summarize内容不同了。
可以看出关键字impl和for,implement这个Summary的trait,是for这个NewArticle和Tweet。
现在我们来使用下。
fn main () {
let article = NewsArticle {
headline: "母猪为何半夜频频嚎叫".to_string(),
location: "未知".to_string(),
author: "走进科学".to_string(),
content: "原来是发情".to_string(),
};
let tweet = Tweet {
username: "川普".to_string(),
content: "China! I love China!!!".to_string(),
reply: true,
retweet: true,
};
println!("article-- {}", article.summarize());
println!("tweet-- {}", tweet.summarize());
}
成功调用,注意这里的trait如果是在别的文件里的,那么你也得导入到当前的文件中。我们来迁移下代码到aggregator.rs这个文件(trate)中。
然后在main.rs里调用

但是报错了

当前scope里NewsArticle里并没有这个method,但是我们确实是有在NewsArticle里实现。
这个时候rust需要我们把Summary这个trait也导入进来。
mod arggregator;
use arggregator::{NewsArticle, Tweet, Summary}; 这样就不会报错了。
为啥要有这样的限制(只有导入到当前的crate才能被implement)呢?
rust开发者管这叫做coherence,翻译过来叫一致性。更贴近的描述是:孤儿规则(orphan rule)。
其实叫做Island孤岛会不会好一点?前端某个框架里就有这种概念。
套在我们的代码里,简单地说就是我们当前crate里的代码不会影响到别的crate里的代码,反过来也一样。
为什么要这么做呢?
因为可能存在多套代码为同一个类型实现同一个trait,但是里面重写的method内容不一样,这样rust压根不知道哪个method是你想调用的。
好了,回到我们的代码中。你应该会觉得这段代码有点搞笑,为什么呢?
因为和struct完全没有区别,都是得去到对应的类型下面自己实现summarize这个方法,所以有没有implement Summary这个trait没差。
实际上真的是这样吗? 我们往下看。
默认的implementation#
额,就是不重写trait里的method,直接沿用。我们来改下我们的NewsArticle这个struct
impl Summary for NewsArticle {} 我们没有重写summarize方法。
不过报错了

为什么会这样呢?看下我们的summarize方法就知道了
fn summarize(&self) -> String;原来我们压根没有去实现,所以也就没有这方法。我们改下
pub trait Summary {
fn summarize(&self) -> String {
"read more".to_string()
}
}
这样就没问题了。
是的,这才是trait的作用:share并且允许override。
有一点需要注意,那就是如果method并没有default implement,那就意味着这个method是必须的(required),也就是说,如果你impl了某个trait,这个trait有些方法没有default implement,那就必须在对应的struct的impl for的block里实现。
将trait作为参数#
还记得昨天使用泛型时,largest方法有一个泛型无法比较的错误吗?那个时候我们就是引入了std::cmp::PartialOrd这个trait,然后赋值给T就没问题了。实际上这也是一种implement。
为泛型T实现这个trait,所以T这个类型就可以用这个trait的比较大小的方法了。
同上,为函数的参数实现trait也是可以的,我们来看个例子
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}我们定义了一个名为notify的function,它的参数item实现了Summary这个trait,所以可以使用summarize这个method。
关键字就是impl。
当然,和泛型中那个只有i32和char能比较的例子一样,有些参数的类型是不能去implement这个trait的,会直接卡在编译的阶段。
为什么呢?
因为这些参数对应的传进来的变量是必须要真的实现了Summary这个trait的,也就是只有Tweet和NewsArticle这两个struct的实例可以作为参数传入notify这个方法中。
所以上面我说的参数implement trait就有些误解了,准确的说应该是类型限制(bound)。
trait约束(bound)语法#
上面item:&impl Summary的写法实际上是下面这种写法的语法糖。
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
} 把T省略了。可以简单的记1 * x == x,把T当做生活中的乘法的1。
不过反过来,这个语法糖有时候不如原始的写法,比如:
pub fn notify(item1: &impl Summary, item2: &impl Summary) { 存在多个参数,这个时候每个都去写一次impl Summary就很麻烦。而原始的写法就会比较简洁
pub fn notify<T: Summary>(item1: &T, item2: &T) { 所以语法糖只能用于简单的场景。
语法:fn fn_name(param: T) -> return type。
多(multiple)trait约束(bound)#
有可能一个trait并不能满足你的要求,所以你可能会implement多个trait,那么你的参数类型自然就得同步约束才不容易出错。
我们来看下例子
pub fn notify(item: &(impl Summary + Display)) {
pub fn notify<T: Summary + Display>(item: &T) {用+符号,相当于&&,表示参数需要同时实现Summary和Display两个trait。
使用where语句来简洁写法#
上面+的写法如果遇到很多个trait就很难受,所以rust官方又提供了一种简洁的写法,就是放到where语句里。
我们来看下例子
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
unimplemented!()
}
这语句相当于
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {两相对比,自然是使用where语句更优雅简洁些。
实际上我们并不是第一次接触,之前我们看的一些标准库里的method的源码就有这个语句,比如unwrap这个方法。

Err变体的参数必须是impl了fmt::Debug这个trait的才行。
返回一个implement了trait的类型#
参数都说了,那自然返回值也是跑不了的。
和参数的类似,我们来看下例子
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}也是impl关键字,返回的数据类型自然是得约束为实现了Summary这个trait的类型。
后面我们会学到Interator和closures,它们用这种写法就比较多。
另外,返回的类型一定得是一样的,即使是都impl了目标trait,如果return的类型不一样也是不行的。
比如:
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle {
headline: String::from(
"Penguins win the Stanley Cup Championship!",
),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
}
} else {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}
} 这样是不行的,返回的数据有两种可能Tweet或者NewsAriticle

使用trait bound有条件的实现method#
我们直接来看例子
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}我们用了两个impl,一个是给关联函数,一个是给cmp_display这个method。
new关联函数需要用到Type但是没必要去impl Display和PartialOrd这俩trait,毕竟只是在生成实例。
但是cmp_display这个method就可能不是所有Type都能使用,所以这个时候用上trait来bound,不允许这个method被乱七八糟的类型调用。
另外,我们也可以给任何的类型impl一个trait,比如
impl<T: Display> ToString for T {
// --snip--
} 给impl了Display这个trait的类型实现一个ToString的trait。
总结#
今天的内容还是有点多的,知识也是比较复杂的。
trait的impl主要的目的是帮助我们减少重复的代码,同时使用trait bound使得trait能固定对应类型(同样compiler也能根据这个bound确认下来类型)。
最后,如果觉得这篇文章对你有帮助的话,请务必点个赞,谢谢~
参考#
- ^rust-defining-share-behavior https://doc.rust-lang.org/book/ch10-02-traits.html#traits-defining-shared-behavior
- ^rust-define-a-trait https://doc.rust-lang.org/book/ch10-02-traits.html#defining-a-trait
- ^rust-implement-a-trait-for-a-type https://doc.rust-lang.org/book/ch10-02-traits.html#implementing-a-trait-on-a-type
- ^rust-use-default-implementation https://doc.rust-lang.org/book/ch10-02-traits.html#default-implementations
- ^rust-use-trait-as-parameters https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters
- ^rust-trait-bound-syntax https://doc.rust-lang.org/book/ch10-02-traits.html#trait-bound-syntax
- ^rust-specify-multiple-trait-bound https://doc.rust-lang.org/book/ch10-02-traits.html#specifying-multiple-trait-bounds-with-the--syntax
- ^rust-clearer-trait-bound-with-where-clauses https://doc.rust-lang.org/book/ch10-02-traits.html#clearer-trait-bounds-with-where-clauses
- ^rust-return-types-that-implement-trait https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits
- ^rust-use-trait-bound-conditionally-implement-methods https://doc.rust-lang.org/book/ch10-02-traits.html#using-trait-bounds-to-conditionally-implement-methods
编辑于 2022-12-16 09:51・IP 属地广东
