Trait
trait
定义了某个特定类型拥有可能与其他类型共享的功能。通过 trait
以抽象的方式定义共同行为。
定义 trait
一个类型的行为由其可供调用的方法构成。对不同类型调用相同的方法,这些类型就可以共享相同的行为了。trait
定义是一种将方法签名组合起来的方法。
pub trait GetInfo {
fn value(&self) -> String;
}
这里使用 trait
关键字来声明一个 trait
,后面跟着的是 trait
的名字,同样可以设置 trait
是否公有。
trait
有点类似于接口,在方法签名后跟分号,而不是在大括号中提供其实现。每一个实现这个 trait
的类型都需要提供其自定义行为的方法体。
trait
中可以有多个方法签名,一行一个方法签名且都以分号结尾。
实现 trait
fn main() {
pub trait GetInfo {
fn value(&self) -> String;
}
pub struct NormalInfo {
pub title: String,
pub content: String,
pub author: String,
}
impl GetInfo for NormalInfo {
fn value(&self) -> String {
format!("title: {}, content: {}, author: {}", self.title, self.content, self.author)
}
}
pub struct AmazingInfo {
pub title: String,
pub reason: String,
pub author: String,
pub is_important: bool,
}
impl GetInfo for AmazingInfo {
fn value(&self) -> String {
format!("title: {}, reason: {}, author: {}, is_important: {}", self.title, self.reason, self.author, self.is_important)
}
}
}
impl
关键字后跟着的是需要实现 trait
的名称,for
后跟着的是需要实现 trait
的类型的名称,也即在某个类型上实现某个 trait
。
默认实现
可以为 trait
中的某些或全部方法提供默认的行为。这样当为某个特定类型实现 trait
时,可以选择保留或重载每个方法的默认行为。
pub trait GetInfo {
fn value(&self) -> String {
String::from("Hello Trait")
}
}
这里给 GetInfo trait
的 value
方法提供了一个默认的字符串值,而不只是定义方法签名了。
如何使用默认实现?
通过指定一个空的 impl
块即可。
pub trait GetInfo {
// 默认实现
fn value(&self) -> String {
String::from("Hello Trait")
}
}
pub struct NormalInfo {
pub title: String,
pub content: String,
pub author: String,
}
// 使用默认实现
impl GetInfo for NormalInfo {}
通过 impl GetInfo for NormalInfo {}
指定了一个空的 impl
块,以此使用默认实现。
使用了默认实现,并不代表将原来的 trait
方法签名给覆盖掉了,而是实例依然能够使用方法签名。
默认实现允许调用相同的 trait
中的其他方法,哪怕这些方法没有默认实现。
trait
作为参数
使用 trait
来接受多种不同类型的参数。
fn main() {
pub trait GetInfo {
fn value(&self) -> String;
}
pub fn console(item: &impl GetInfo) {
println!("{}", item.value());
}
}
参数 item
指定了 impl
关键字和 GetInfo trait
,该参数支持任何实现了指定 trait
的类型。在 console
函数体中,可以调用任何来自 GetInfo trait
的方法。
Trait Bound
trait bound
实际上是一种较长形式的语法糖。
fn main() {
pub trait GetInfo {
fn value(&self) -> String;
}
// trait bound
pub fn console<T: GetInfo>(item: &T) {
println!("{}", item.value());
}
}
相较于前面的例子,trait bound
与泛型参数声明在一起,位于尖括号中的冒号后面。
与 impl Trait
相比,trait bound
更适合用于复杂的场景,而 impl Trait
适用于短小的场景:
fn main() {
pub trait GetInfo {
fn value(&self) -> String;
}
// impl trait 语法
pub fn console(item1: &impl GetInfo, item2: &impl GetInfo) {
println!("item1: {}, item2: {}", item1.value(), item2.value());
}
// trait bound 语法
pub fn console2<T: GetInfo>(item1: &T, item2: &T) {
println!("item1: {}, item2: {}", item1.value(), item2.value());
}
}
从代码中可以看出,impl Trait
语法在函数参数的角度来说,确实是更长一点,并且 item1
和 item2
允许是不同类型的情况,只要它们都实现了 GetInfo
。而 trait bound
语法在函数参数的角度则更短一点,并且 item1
和 item2
是相同的类型。
通过
+
指定多个trait bound
+
也可以适用于 impl Trait
语法。
fn main() {
pub trait GetInfo {
fn value(&self) -> String;
}
pub trait GetMessage {
fn message(&self) -> String;
}
pub fn console(item: &(impl GetInfo + GetMessage)) {
println!("Value: {}, Message: {}", item.value(), item.message());
}
}
通过 +
指定多个 trait bound
fn main() {
pub trait GetInfo {
fn value(&self) -> String;
}
pub trait GetMessage {
fn value(&self) -> String;
}
pub fn console<T: GetInfo + GetMessage>(item: &T) {
println!("Value: {}, Message: {}", item.value(), item.message());
}
}
看着还是很清晰的~
由于 trait bound
是一种较长形式的语法糖,所以当过多的 trait bound
也有缺点,每个泛型有自己的 trait bound
,所以有多个泛型参数的函数在函数名称和参数列表之间会有很长的 trait bound
信息,这导致函数签名难以阅读。于是,Rust 给出了一个解决办法——使用 where
简化过多的 trait bound
使用
where
简化trait bound
未使用 where
简化前:
fn main() {
pub trait GetInfo {
fn value(&self) -> String;
}
pub trait GetMessage {
fn message(&self) -> String;
}
pub trait GetResult {
fn result(&self) -> String;
}
pub fn console<T: GetInfo + GetMessage, E: GetMessage + GetResult>(t: &T, e: &E) {
println!("Value: {}, Message: {}, Result: {}", t.value(), t.message(), e.result());
}
}
可以看到 trait bound
非常长,这还是只有 3 个 trait
的时候...
使用 where
简化 trait bound
:
fn main() {
pub trait GetInfo {
fn value(&self) -> String;
}
pub trait GetMessage {
fn message(&self) -> String;
}
pub trait GetResult {
fn result(&self) -> String;
}
// 使用 where 简化 trait bound
pub fn console<T, E>(t: &T, e: &E)
where
T: GetInfo + GetMessage,
E: GetMessage + GetResult,
{
println!("Value: {}, Message: {}, Result: {}", t.value(), t.message(), e.result());
}
}
看起来好像复杂了,其实也还好,但是使用 where
简化 trait bound
确实达到了目的,确实解决了之前提到的问题。
返回实现了 trait
的类型
可以在返回值中使用 impl Trait
语法,来返回实现了某个 trait
的类型:
fn main() {
pub trait GetInfo {
fn value(&self) -> String;
}
pub struct NormalInfo {
pub title: String,
pub content: String,
pub author: String,
}
impl GetInfo for NormalInfo {
fn value(&self) -> String {
format!("title: {}, content: {}, author: {}", self.title, self.content, self.author);
}
}
fn reture() -> impl GetInfo {
NormalInfo {
title: String::from("Title"),
content: String::from("Content"),
author: String::from("Author"),
}
}
}
通过 impl GetInfo
作为返回值类型,指定了 return
函数返回某个实现了 GetInfo trait
的类型,但是不确定具体的类型。
对任何实现了特定的 trait
的类型有条件地实现 trait
,好绕啊?你可以理解为在 trait
上实现 trait
,只不过某个 trait
是有类型限制的。
impl<T: GetInfo> ToString for T {
// ...
}
在实现了 GetInfo trait
的类型上实现了 ToString trait
。
对于任何满足特定 trait bound
的类型实现 trait
,也即在 trait bound
上实现 trait
,这种行为被称为 blanket implementations
(一揽子实现或覆盖实现)。
这个的作用就是可以对任何实现了 GetInfo trait
的类型调用 ToString
类型的 to_string
方法。
总结
trait
和 trait bound
使用泛型类型参数来减少重复,并且泛型还拥有单态化的特性,因此泛型可以使得代码更具通用性和灵活性,以及更好的性能。编译器利用 trait bound
在编译时检查代码中所用到的具体类型是否提供了正确的行为,这避免了在运行时出现错误。