1. 概述
Trait告诉Rust编译器,某种类型具有哪些并且可以与其他类型共享的功能,可以使用抽象的方式定义共享行为。Trait bounds
(约束)指的是泛型类型参数指定实现了特定行为的类型。
Trait与其他语言的借口(interface)类似,但是有区别。
2. 定义Trait
定义Trait的目的是把方法签名放在一起,来实现某种目的所必须的一组行为。定义trait的相关规则如下
- 关键字:trait
- 只有方法签名,没有具体实现
- trait可以有多个方法,每个方法占一行,以
;
结尾 - 实现该trait的类型必须提供具体方法的实现
如下定义trait的示例代码
pub trait Summary {
fn summarize(&self) -> String;
}
3. 在类型上实现trait
与类型的实现方法类似,如下示例代码
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewArticle {
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)
}
}
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false
};
println!("1 new tweet: {}", tweet.summarize());
}
4. 实现某个trait的约束
可以在某个类型上实现某个trait的前提条件为:这个类型或这个trait在本地crate里定义的。
无法为外部类型来实现外部的trait,这个限制是程序属性的一部分(也就是一致性),更具体地说是孤儿原则,之所以这样命名是因为父类型不存在。此规则确保他人的代码不能破坏你写的代码,反之亦然。如果没有这个规则,两个crate可以为同一个类型实现同一个trait,rust就不知道应该使用哪个实现了。
5. 默认实现
trait可以拥有默认实现,当我们为某个类型实现trait时,可以选择重载或者保留原有的默认实现。默认实现的方法也可以调用trait中其他的方法,即使这些方法没有默认实现。如下示例代码
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct NewArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewArticle {
fn summarize_author(&self) -> String {
format!("@{} ", self.author)
}
}
需要注意的是,我们无法在方法的重写实现里调用默认的实现。
6. 使用trait作为参数
6.1 impl Trait
使用impl Trait
的语法,可以适用于简单的函数传参情况。加入我们希望传入的参数实现了一个Summary
这个trait,那么我们可以使用如下的参数声明方式
pub fn notify(item: impl Summary) {
println("breaking news! {}", item.summarize());
}
那么我们在调用此函数的使用,可以传入Tweet
,也可以传入NewArticle
。
6.2 Trait bound
使用Trait bound
可以与复杂的情况,如下示例代码
pub fn notify<T: Summary>(item: T) {
println("breaking news! {}", item.summarize());
}
如果包含两个参数,使用Trait bound
的写法如下
pub fn notify<T: Summary>(itema: T, item2: T) {
println("breaking news! {}", item1.summarize());
}
实际上impl Trait
是Trait bound
的语法糖。
6.3 使用+指定多个trait bound
如果使用impl Trait
的方式,示例代码如下
use std::fmt::Display;
pub fn notify(item: impl Summary + Display) {
println("breaking news! {}", item.summarize());
}
使用Trait bound
的写法如下
use std::fmt::Display;
pub fn notify<T: Summary + Display>(item: T) {
println("breaking news! {}", item.summarize());
}
6.4 在方法签名后面使用where子句
如果一个函数要求的变量需要实现过多的Trait,代码的可读性会变低,很不直观,如下示例代码
pub fn notify<T: Summary + Display, U: Clone + Debug>(a: T, b: U) -> String {
format!("breaking news! {}", a.summarize());
}
我们可以使用where子句还简化Trait的约束,如下示例代码
pub fn notify<T, U>(a: T, b: U) -> String
where T: Summary + Display,
U: Clone + Debug,
{
format!("breaking news! {}", a.summarize());
}
7. 使用trait作为返回值类型
使用impl Trait
声明返回的类型为Trait,如下示例代码
pub fn notify(flag: bool) -> impl Summary {
// ......
}
使用impl Trait
只能返回确定的同一种类型,返回可能不同类型的代码会报错,如下代码将报错
pub fn notify(flag: bool) -> impl Summary {
if flag {
NewArticle {
// ......
}
} else {
Tweet {
// ......
}
}
}
8. 使用PartialOrd实现数据比较
如下示例代码
use std::result;
fn largest<T: PartialOrd +Clone>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list.iter() {
if item > &largest {
largest = item;
}
}
largest
}
fn main() {
let str_list = vec![String::from("hello"), String::from("world")];
let result = largest(&str_list);
println!("The largest word id {}", result);
}
这段代码定义了一个泛型函数largest,该函数接受一个类型为[T]的切片参数list,并返回切片中最大的元素的引用。
在函数定义中,我们为泛型参数T添加了两个trait bound,PartialOrd和Clone。PartialOrd trait指定了T类型必须支持比较操作符(如>, <等),而Clone trait指定T类型必须支持克隆操作。
在函数体中,我们首先将largest初始化为切片list中的第一个元素的引用。然后,我们遍历切片中的每个元素,并与largest进行比较。
注意,我们使用了&largest,因为largest是一个引用,而item是一个值。通过使用&,我们可以将item转换为一个引用进行比较。
如果item大于largest,我们更新largest为item。最后,我们返回largest。
由于largest函数使用了PartialOrd和Clone trait,它可以用于任意支持比较和克隆的类型。在这种情况下,我们使用了String类型,因为它满足这些要求。但是,如果我们传递了一个不支持比较或克隆的类型的切片,代码将无法编译。
9. 使用Trait Bound有条件的实现方法
在使用泛型类型参数的 impl 块上使用Trait bound,我们可以有条件的为了实现特定 trait 的类型来实现。如下示例代码
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
// 所有Pair<T> 都拥有new函数
impl <T> Pair<T>{
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
// 实现了Display 和 PartialOrd 的Pair<T> 猜拥有com_display函数
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);
}
}
}
fn main(){
}
也可以为实现其他Trait的类型有条件的实现某个Trait。为满足Trait Bound的所有类型上实现Trait叫做覆盖实现(Blanket implementations)。
10. 总结
Trait是Rust中的一种抽象机制,用于定义共享行为并与类型进行交互。Trait可以看作是一组方法签名的集合,它定义了类型必须实现的方法。
Trait的用法包括以下几个方面:
-
定义Trait:使用
trait
关键字定义Trait,指定方法的签名。Trait中的方法没有具体的实现,只有方法签名。 -
实现Trait:使用
impl
关键字在类型上实现Trait,提供方法的具体实现。实现Trait时,方法名和参数列表必须与Trait定义中的方法签名一致。 -
默认实现:Trait可以具有默认实现,当为类型实现Trait时,可以选择重写Trait中的方法或保留默认实现。
-
Trait作为函数参数:函数可以接受Trait作为参数类型,这样函数可以处理不同的类型,只要这些类型实现了所需的Trait。
-
Trait作为返回值类型:函数可以返回Trait作为返回值类型,这样函数可以根据具体情况返回不同的实现Trait的类型。
-
Trait作为泛型约束:通过Trait约束,可以限制泛型类型参数必须实现特定的Trait,从而在编译时保证类型的一致性。
-
多个Trait约束:可以在泛型约束中指定多个Trait,使用
+
来连接不同的Trait。 -
Trait的继承:一个Trait可以继承另一个Trait,使用
trait SubTrait: SuperTrait
的语法来指定Trait的继承关系。
Trait的使用可以提高代码的可读性和重用性,使得代码更加模块化和可扩展。通过定义和实现Trait,可以定义通用的行为,使得代码更具有灵活性和可维护性。