trait是rust非常重要的知识点
trait
用于定义与其它类型共享的功能,类似于其它语言中的接口。
trait
以一种抽象的方式定义共享的行为。trait bounds
指定泛型是任何拥有特定行为的类型。一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait
定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。
例如,老师和学校之间他们有的共同行为是都有自己的名称和年龄,不同行为例如老师有教的科目和学生所在的年级:
pub trait GetInformation {
fn get_name(&self) -> &String;
fn get_age(&self) -> &u32;
}
这里使用 trait
关键字来声明一个trait
,后面是trait
的名字,在这个例子中是 GetInformation
。我们也声明trait
为pub
以便依赖这个 crate
的 crate
也可以使用这个 trait
。在大括号中声明描述实现这个trait
的类型所需要的行为的方法签名,在这个例子中是 get_name
,get_age
,trait
里面的函数可以没有函数体,实现代码交给具体实现它的类型去补充。trait
体中可以有多个方法:一行一个方法签名且都以分号结尾。
如果类型实现这个trait
的类型就必须提供其自定义行为的方法体,否则编译不通过:
pub trait GetInformation {
fn get_name(&self) -> &String;
fn get_age(&self) -> u32;
}
// 老师
pub struct Teacher {
name: String,
age: u32,
subject: String,
}
impl GetInformation for Teacher {
fn get_name(&self) -> &String {
&self.name
}
fn get_age(&self) -> u32 {
self.age
}
}
impl Teacher {
fn get_subject(&self) -> &String {
&self.subject
}
}
// 学生
pub struct Student {
name: String,
age: u32,
grade: String,
}
impl GetInformation for Student {
fn get_name(&self) -> &String {
&self.name
}
fn get_age(&self) -> u32 {
self.age
}
}
impl Student {
fn get_grade(&self) -> &String {
&self.grade
}
}
fn main() {
let s = Student {
name: String::from("李四"),
age: 9,
grade: String::from("六年级"),
};
println!(
"学生 name = {},age = {},grade = {}",
s.get_name(),
s.get_age(),
s.get_grade()
);
let t = Teacher {
name: String::from("罗翔"),
age: 36,
subject: String::from("律师"),
};
println!(
"老师 name = {},age = {},subject = {}",
t.get_name(),
t.get_age(),
t.get_subject()
);
}
在类型上实现 trait
类似于实现与 trait
无关的方法。区别在于 impl
关键字之后,我们提供需要实现 trait
的名称,接着是for
和需要实现 trait
的类型的名称。在 impl
块中,使用trait
定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现trait
方法所拥有的行为。
有时为trait
中的某些或全部方法提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现trait
时,可以选择保留或重载每个方法的默认行为:
pub trait GetInformation {
fn get_name(&self) -> &String;
fn get_age(&self) -> u32;
fn get_complete(&self) -> String {
format!(
"name = {},age = {},school = {}",
self.get_name(),
self.get_age(),
String::from("北京大学")
)
}
}
// 老师
pub struct Teacher {
name: String,
age: u32,
subject: String,
}
impl GetInformation for Teacher {
fn get_name(&self) -> &String {
&self.name
}
fn get_age(&self) -> u32 {
self.age
}
}
impl Teacher {
fn get_subject(&self) -> &String {
&self.subject
}
}
// 学生
pub struct Student {
name: String,
age: u32,
grade: String,
}
impl GetInformation for Student {
fn get_name(&self) -> &String {
&self.name
}
fn get_age(&self) -> u32 {
self.age
}
}
impl Student {
fn get_grade(&self) -> &String {
&self.grade
}
}
fn main() {
let s = Student {
name: String::from("李四"),
age: 9,
grade: String::from("六年级"),
};
println!("{}", s.get_complete());
let t = Teacher {
name: String::from("罗翔"),
age: 36,
subject: String::from("律师"),
};
println!("{}", t.get_complete());
}
如果想要对GetInformation
实例使用这个默认实现,可以通过 impl GetInformation for Teacher {}
指定一个空的impl
块,如果已经存在impl
块可以不用实现默认方法。
默认实现允许调用相同 trait
中的其他方法,哪怕这些方法没有默认实现。如此,trait
可以提供很多有用的功能而只需要实现指定一小部分内容。
知道了如何定义trait
和在类型上实现这些 trait
之后,我们可以探索一下如何使用 trait
来作为参数:
......
fn print_information(itme: impl GetInformation) {
println!("{}", itme.get_complete())
}
fn main() {
let s = Student {
name: String::from("李四"),
age: 9,
grade: String::from("六年级"),
};
print_information(s);
let t = Teacher {
name: String::from("罗翔"),
age: 36,
subject: String::from("律师"),
};
print_information(t);
}
对于item
参数,我们指定了 impl
关键字和 trait
名称,而不是具体的类型。该参数支持任何实现了指定 trait
的类型。在print_information
函数体中,可以调用任何来自 GetInformation trai
t 的方法,比如 print_information
。我们可以传递任何 Student
或 Teacher
的实例来调用 print_information
。任何用其它如 String
或 i32
的类型调用该函数的代码都不能编译,因为它们没有实现 GetInformation
。
impl Trait
语法适用于直观的例子,它实际上是一种较长形式语法的语法糖。我们称为 trait bound
,它看起来像:
fn print_information<T: GetInformation>(itme: T) {
println!("{}", itme.get_complete())
}
这与之前的例子相同,不过稍微冗长了一些。trait bound
与泛型参数声明在一起,位于尖括号中的冒号后面。
impl Trait
很方便,适用于短小的例子。更长的 trait bound
则适用于更复杂的场景。例如,可以获取两个实现了 GetInformation
的参数。使用 impl Trait
的语法看起来像这样:
......
fn print_information<T: GetInformation>(itme1: T, itme2: T) {
println!("{}", itme1.get_complete());
println!("{}", itme2.get_complete());
}
fn main() {
let s1 = Student {
name: String::from("李四1"),
age: 9,
grade: String::from("六年级"),
};
let s2 = Student {
name: String::from("李四2"),
age: 9,
grade: String::from("六年级"),
};
print_information(s1, s2)
}
泛型 T
被指定为 item1
和 item2
的参数限制,如此传递给参数 item1
和 item2
值的具体类型必须一致。
如果 print_information
需要 item
的GetName
,同时也要使用 GetAge
方法,那么 item
就需要同时实现两个不同的 trait:GetName
和 GetAge
。这可以通过+
语法实现:
pub trait GetName {
fn get_name(&self) -> &String;
}
pub trait GetAge {
fn get_age(&self) -> u32;
}
// 老师
pub struct Teacher {
name: String,
age: u32,
subject: String,
}
impl GetName for Teacher {
fn get_name(&self) -> &String {
&self.name
}
}
impl GetAge for Teacher {
fn get_age(&self) -> u32 {
self.age
}
}
fn print_information<T: GetName + GetAge>(itme: T) {
println!("{}", itme.get_name());
println!("{}", itme.get_age());
}
fn main() {
let t = Teacher {
name: String::from("罗翔"),
age: 36,
subject: String::from("律师"),
};
print_information(t);
}
Teacher
必须实现GetName
和 GetAge
的trait
,否则将无法调用print_information
函数,<T: GetName + GetAge>
更像调用的条件。
然而,使用过多的trait bound
也有缺点。每个泛型有其自己的 trait bound
,所以有多个泛型参数的函数在名称和参数列表之间会有很长的 trait boun
d 信息,这使得函数签名难以阅读。为此,Rust
有另一个在函数签名之后的 where
从句中指定 trait bound
的语法。所以除了这么写:
fn print_information<T: GetName, U: GetAge>(itme1: T, itme2: U) {
println!("{}", itme1.get_name());
println!("{}", itme2.get_age());
}
还可以像这样使用 where
从句:
fn print_information<T, U>(itme1: T, itme2: U)
where
T: GetName,
U: GetAge,
{
println!("{}", itme1.get_name());
println!("{}", itme2.get_age());
}
这个函数签名就显得不那么杂乱,函数名、参数列表和返回值类型都离得很近,看起来跟没有那么多 trait bounds
的函数很像。
也可以在返回值中使用impl Trait
语法,来返回实现了某个 trait
的类型:
fn produce_item_with_age() -> impl GetAge {
Teacher {
name: String::from("罗翔"),
age: 36,
subject: String::from("律师"),
}
}
通过使用impl GetAge
作为返回值类型,我们指定了 produce_item_with_age
函数返回某个实现了GetAge trait
的类型,但是不确定其具体的类型。在这个例子中 produce_item_with_age
返回了一个 Teacher
,不过调用方并不知情。
不过这只适用于返回单一类型的情况。例如,这段代码的返回值类型指定为返回 impl GetAge
,但是返回了 Teacher
或 Student
就行不通:
pub trait GetName {
fn get_name(&self) -> &String;
}
pub trait GetAge {
fn get_age(&self) -> u32;
}
// 老师
pub struct Teacher {
name: String,
age: u32,
subject: String,
}
impl GetAge for Teacher {
fn get_age(&self) -> u32 {
self.age
}
}
// 学生
pub struct Student {
name: String,
age: u32,
grade: String,
}
impl GetAge for Student {
fn get_age(&self) -> u32 {
self.age
}
}
fn produce_item_with_age(b: bool) -> impl GetAge {
if b {
Teacher {
name: String::from("罗翔"),
age: 36,
subject: String::from("律师"),
}
} else {
Student {
name: String::from("李四"),
age: 9,
grade: String::from("六年级"),
}
}
}
fn main() {
let t = produce_item_with_age();
}
这里尝试返回Teacher
或 Student
。这不能编译,因为 impl Trait
工作方式的限制。
在泛型类型章节我们做过一个功能就是寻找 slice
中最大值:
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![11, 232, 63, 74, 51, 96];
let result = largest(&number_list);
println!("最大的数字是:{}", result);
let char_list = vec!['a', 't', 'k', 's', 'x', 'l'];
let result = largest(&char_list);
println!("最大的字符是:{}", result);
}
largest
函数在它的签名中使用了泛型,统一了两个实现,这个时候我们需要给泛型T
加上trait
特性限制:
fn largest<T>(list: &[T]) -> T
where
T: PartialOrd + Copy,
{
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
这里我们限制T
必须实现比较和复制的trait
。
通过使用带有trait bound
的泛型参数的 impl
块,可以有条件地只为那些实现了特定 trait
的类型实现方法:
trait GetName {
fn get_name(&self) -> &String;
}
trait GetSubject {
fn get_subject(&self) -> &String;
}
trait GetGrade {
fn get_grade(&self) -> &String;
}
// 学校
struct School<T, S> {
teacher: T,
student: S,
}
impl<T, S> School<T, S>
where
T: GetName + GetSubject,
S: GetName + GetGrade,
{
fn print_school(&self) {
println!("{}-{}", self.teacher.get_name(), self.teacher.get_subject());
println!("{}-{}", self.student.get_name(), self.student.get_grade());
}
}
// 老师
pub struct Teacher {
name: String,
age: u32,
subject: String,
}
impl GetName for Teacher {
fn get_name(&self) -> &String {
&self.name
}
}
impl GetSubject for Teacher {
fn get_subject(&self) -> &String {
&self.subject
}
}
// 学生
pub struct Student {
name: String,
age: u32,
grade: String,
}
impl GetName for Student {
fn get_name(&self) -> &String {
&self.name
}
}
impl GetGrade for Student {
fn get_grade(&self) -> &String {
&self.grade
}
}
fn main() {
let s = Student {
name: String::from("李四"),
age: 9,
grade: String::from("六年级"),
};
let t = Teacher {
name: String::from("罗翔"),
age: 36,
subject: String::from("律师"),
};
let school = School {
teacher: t,
student: s,
};
school.print_school();
}
School
结构体的print_school
方法的特性是T
必须实现GetName
和GetSubject
,U
必须实现GetName
和GetGrade
的trait
才能调用print_school
方法。
因为我们向编译器提供了 trait bound
信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中,如果我们尝试调用一个类型并没有实现的方法,会在运行时出现错误。Rust
将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复错误。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了,这样相比其他那些不愿放弃泛型灵活性的语言有更好的性能。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!