Rust Trait 的使用
trait(特征)被用来向rust编译器描述某些特定类型拥有的且能够被其他类型共享的功能,它使我们可以以一种抽象的方式来定义共享行为。我们还可以使用trait约束来将泛型参数指定为实现了某些特定行为的类型。
定义trait
定义一个trait:
pub trait Summary { fn summarize(&self) -> String; }
在trait中定义方法签名,可以定义多个方法签名。
为类型实现trait
#![allow(unused_variables)] fn main() { 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) } } }
实现trait需要在impl关键字后提供我们想要实现的trait名,并紧接这for关键字及当前的类型名。在impl代码块中,我们同样需要填入trait中的方法,在花括号中实现针对该结构体的函数体。
如何我们便可以基于实例调用该trait的方法了:
use chapter10::{self, Summary, Tweet}; 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()); }
trait有一个限制:只有当trait或类型定义于我们的库中时,我们才能为该类型实现对应的trait。
我们不能为外部类实现外部trait,这个限制被称为孤儿规则,因为他的父类型没有定义在当前库中。这一规则也是程序一致性的组成部分,确保了其他所有的人所编写的内容不会破坏到你的代码。如果没有这个规则,那么两个库可以分别对相同的类型实现相同的trait,rust无法确定使用哪一个版本。
默认实现
trait可以提供一个默认的实现,这样使用trait的类型就不用非要自己写实现了:
impl Summary for Tweet { fn summarize(&self) -> String { format!("{}: {}", self.username, self.content) } }
语法上就是把签名的分号去掉,任何在花括号中写上函数体即可。
如果我们决定使用默认实现,而不自定义实现,那么就可以指定一个空的impl代码块:
impl Summary for NewsArticle {}
更骚的操作:
我们可以在默认实现中调用其他默认实现,哪怕这个实现里面是空的。在这个空的默认实现被实例实现后,trait又能有更强的多样性。
#![allow(unused_variables)] fn main() { pub trait Summary { fn summarize_author(&self) -> String; fn summarize(&self) -> String { format!("(Read more from {}...)", self.summarize_author()) } } pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool, } impl Summary for Tweet { fn summarize_author(&self) -> String { format!("@{}", self.username) } } }
使用trait作为参数
将trait作为参数,可以使实现了trait的类型传递进来。没有实现该trait的类型则不行。
pub fn notify(item: &impl Summary) { println!("Breaking news! {}", item.summarize()); }
你可能发现了,trait作为参数使用了泛型。
trait约束
上面的代码其实是trait约束的一种语法糖,它的完整形式其实是下面这样:
pub fn notify<T: Summary>(item: &T) { println!("Breaking news! {}", item.summarize()); }
有一种情况是一定要用完整的trait约束,而不能使用语法糖的,举个例子:
我们要使用两个trait参数
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
这样写,item1和item2可以是两种不同的类型,如果你一定要item1和item2是相同的类型,那么只能用完整的约束:
pub fn notify<T: Summary>(item1: &T, item2: &T) {
通过+语法来指定多个trait约束
类型可以实现多个trait。传参的时候也可以规定传进来的类型需要实现多个trait,那么我们可以使用+语法来指定多个trait约束。
语法糖写法:
pub fn notify(item: &(impl Summary + Display)) {
完整trait约束写法:
pub fn notify<T: Summary + Display>(item: &T) {
使用where从句来简化trait约束
实现多个trait:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
太多的trait会使函数签名臃肿且难以理解,所以加入where子句来整理trait约束:
fn some_function<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug {
返回实现了trait的类型
返回值中同样可以返回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, } }
但是,下面这种代码依旧不能成功编译,NewsArticle和Tweet都实现了impl Summary,却依然无法通过编译:
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) } } 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, } } }
碍于impl Trait工作方式的限制,rust不支持这种代码。之后会讨论。
(方法返回的类型不能是self,不能包含任何泛型参数,不然对象不安全。单态化使得trait忘记了自己self的具体类型)
额外:返回trait类型的引用
返回trait类型和返回trait类型的引用是不同的:
&(impl tra)
impl test{ fn ttt(&self) -> &(impl tra){ return self; } }
使用trait约束来修复largest函数
在上一章中泛型数据类型,我们提到std::cmp::PartialOrd,现在让我们来修复这个错误。
无非就是让传进来的参数是实现了PartialOrd的类型,改一下函数签名就可以了:
fn largest<T: PartialOrd>(list: &[T]) -> T {//改一下函数签名 let mut largest = list[0]; for &item in list { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!("The largest number is {}", result); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest(&char_list); println!("The largest char is {}", result); } }
但是不行,会报错:
$ cargo run Compiling chapter10 v0.1.0 (file:///projects/chapter10) error[E0508]: cannot move out of type `[T]`, a non-copy slice --> src/main.rs:2:23 | 2 | let mut largest = list[0]; | ^^^^^^^ | | | cannot move out of here | move occurs because `list[_]` has type `T`, which does not implement the `Copy` trait | help: consider borrowing here: `&list[0]` error[E0507]: cannot move out of a shared reference --> src/main.rs:4:18 | 4 | for &item in list { | ----- ^^^^ | || | |data moved here | |move occurs because `item` has type `T`, which does not implement the `Copy` trait | help: consider removing the `&`: `item` error: aborting due to 2 previous errors Some errors have detailed explanations: E0507, E0508. For more information about an error, try `rustc --explain E0507`. error: could not compile `chapter10`. To learn more, run the command again with --verbose.
大概就是说 cannot move out of type T。
原因在于我们写了:let mut largest = list[0];
这一句在没有实现Copy trait的类型运行时会发生move。
但是我们无法将list[0]中的值一处并绑定到largest变量上。(比如String)
那么我们加上一个Copy trait呗,缩小传入参数的范围:
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {//改一下函数签名 let mut largest = list[0]; for &item in list { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!("The largest number is {}", result); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest(&char_list); println!("The largest char is {}", result); }
当然如果你如果想支持更多的数据类型,可以把Copy trait改成Clone trait。
使用trait约束来有条件地实现方法
rust支持对一个类型写多个impl代码块,所以我们可以单独为实现了指定trait的类型写方法。
比如如下代码,只有T实现了PartialOrd和Display这两个trait时才能实现cmd_display方法:
#![allow(unused_variables)] fn main() { 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); } } } }
覆盖实现
对满足trait约束的所有类型实现trait被称作覆盖实现。
其实覆盖实现就是批量为很多类型实现trait方法,但表述为覆盖总觉得不太对劲。
代码如下,利用了for+泛型来实现:
impl<T: Display> ToString for T { // --snip-- }
最典型的就是为实现了Display trait的类型调用ToString trait中的to_String方法。
模块化有助于代码的管理和层次逻辑的清晰Rust模块化有多种方式: 1. 嵌套模块嵌套模块就是直接在要使用模块的文件中声明模块mod food{//声明模块 pub struct Cake; pub struct ...