rust学习十一.2、利用Trait(特质)定义通用类型的共同行为
Trait 本意是特性,特质,特征等等,其实主要指人的性格特征。不明白为什么rust的创造者不使用feature这样单词。
如作者所言:
Note: Traits are similar to a feature often called interfaces in other languages, although with some differences.
特征类似于其它语言的接口,但和接口还是有一些区别的。
为了便于行文,本文把Trait翻译为特质。
我查了一些资料,可以确认这句话基本上是对的。不对的在哪里了?此处先不聊了。 总之特质在大部分情况下可以当做接口即可。
但是不能把特质称为接口,因为接口仅仅是特质的一个功能,它还有其它作用。
本文的内容主要都是为了通用类型服务,前面讲了很多接口特质的内容。
从本章开始,可以看到越来越多奇奇怪怪的语法,虽然我已经学过不少语言,但Rust绝对是其中的奇葩!
一、如何定义一个接口特质
所谓接口特质,即作为接口使用的特质。
就一个步骤:
pub trait Work{ fn design(&self); fn code(&self); fn test(&self); }
1.pub修饰符可选
2.一个接口特质中可以定义多个方法
这和大部分语言差不多。
二、为结构体实现一个接口特质
2.1基本实现
实现一个接口特质也很简单,利用impl语法:
几个注意事项:
1.当实现一个接口特质的时候,必须实现这个接口特质中的所有方法,不能只有一部分
2.不能越界实现其它单元包中的接口特质。例如在单元包a存在接口特质 Ta,那么无法在单元包b中实现Ta
如果违反了,会提示:only traits defined in the current crate can be implemented for types defined outside of the crate
define and implement a trait or new type instead
3.如果接口特质T在当前单元包,那么无论对象(结构体、枚举等)O位于哪里,那么都可以为O实现T
4.在同一个单元包内,你不能在不同模块为同个对象实现多次接口特质
以上第2条并不是普适的。rust的一些特质是位于标准库中,但允许你在自己的模块中实现这些接口特质,典型的是Display
2.2 默认实现
和java一样(从J8开始),rust也提供了默认实现,只不过java把接口搞得更加复杂一些。
trait Summary { fn summarize(&self) -> String; fn get_content(&self) -> &String; fn is_empty(&self) -> bool { self.get_content().is_empty() } }
方法is_empty就是默认的实现。这样在实现代码中,不需要提供is_empty有关的代码,也可以正常使用:
trait Summary { fn summarize(&self) -> String; fn get_content(&self) -> &String; fn is_empty(&self) -> bool { self.get_content().is_empty() } } struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String } impl Summary for NewsArticle { fn summarize(&self) -> String { format!("{}, by {} ({}) \n {}", self.headline, self.author, self.location,self.content) } fn get_content(&self) -> &String { &self.content } } fn main(){ let news= NewsArticle { headline: String::from("英雄纪念碑"), location: String::from("中国北京"), author: String::from("新华社记者m--泽--东"), content: String::from("由此上溯到一千八百四十年,从那时起,为了反对内外敌人, 争取民族独立和人民自由幸福,在历次斗争中牺牲的人民英雄们永垂不朽!") }; println!("{}", news.summarize()); println!("{}", news.is_empty()); }
默认方法是否可以覆盖了?可以的,这个和java也一样:
impl Summary for NewsArticle { fn summarize(&self) -> String { format!("{}, by {} ({}) \n {}", self.headline, self.author, self.location,self.content) } fn get_content(&self) -> &String { &self.content } fn is_empty(&self) -> bool { println!("{}", self.content.len()); self.content.len()<1000 } }
默认实现,减少了打码量,尤其是这个接口特质可能被许多对象实现的时候。
三、接口特质作为一个参数
这里讨论几个问题:
- 如何把一个接口特质作为一个参数,或者说代码上如何写?语法是什么
- 如果要求所有的参数实现同个接口特质,且是同种类型,要怎么写?
- 如果参数要求实现多个接口特质怎么定义。即某个参数P需要实现接口特质T1,T2,..Tn,那么如何书写(非如何为P实现特质接口T1,T2..Tn)?
3.1 定义-简化形式
语法如下: fn xxx( p: impl ***)
其中xxx是方法名,p是变量名,***是特质名
fn print(article:&impl Summary){ println!("{}", article.get_content()); }
语法比较怪异,这都rust自己挖坑,自己跳。不过比起一些故意折腾人的语言,也还可以将就接受。 也就是因为这个,rustc被设计的特别强大,编译信息也特别贴心,否则这些奇奇怪怪的不容易记忆。
如果是java等比较人性,也简单: 例如 public get(IBook:book)
如果没有什么特别要求,那么就这样写吧!
3.2 定义-正规形式,及其特有作用
书上说,impl 语法是一个语法糖(我认为不是太合适。真要那么想,基本上所有的都是语法糖,只能说有多种形式),正规的写法是:fn xxxx<T:t>(p:t)
就是在方法后直接跟上<T:t>这样的形式。
例如上面那个print,可以写成如下:
/** * 使用<T:t>的方式,是impl trait的方式的正规形式。 * 即print_normal是print的正规形式 */ fn print_normal<T:Summary>(article:&T){ println!("使用<T:t>的方式:{}", article.get_content()); }
这种正规形式的另外一个用处:如果方法带有多个实现了指定接口特质的参数,同时要求这几个参数都是同个类型,那么必须使用正规格式。
来个例子:
例子1- 报错的例子
struct Box{vol: u32} struct tube{x:u32,y:u32, z:u32,vol: u32} trait Brush{fn draw(&self);} impl Brush for Box{ fn draw(&self){ println!("draw box"); } } impl Brush for tube{ fn draw(&self){ println!("draw tube: x:{},y:{},z:{}"); } } fn draw_objet(b1:&impl Brush, b2:&impl Brush){b1.draw();b2.draw();} fn draw_objet2<T:Brush>(b1:&T, b2:&T){b1.draw();b2.draw();} fn main() { let b1 = Box{vol: 20}; let t1 = tube{x:5, y:3, z:4, vol: 10}; draw_objet(&b1,&t1); draw_objet2(&b1,&t1); }
执行后,编译报错:
提示的很清楚了,这是因为 draw_objet2要求两个参数一致,但现在不一致,一个是&Box,一个是&Cube
但是draw_object没有这个问题,它使用的是简单形式。
3.3 如何限定一个参数必须实现多个接口特质
方式有2个:
- 使用+号,连接多个接口特质
- 使用where字句
示例
use std::fmt::Display; use std::fmt; struct Box{vol: u32} struct tube{x:u32,y:u32, z:u32,vol: u32} trait Brush{fn draw(&self);} impl Brush for Box{ fn draw(&self){ println!("draw box"); } } impl Brush for tube{ fn draw(&self){ println!("{}",self); } } impl fmt::Display for tube{ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f,"x:{},y:{},z:{}.体积={}",self.x,self.y,self.z,self.vol) } } fn draw_objet(b1:&impl Brush, b2:&impl Brush){b1.draw();b2.draw();} fn draw_objet2<T:Brush>(b1:&T, b2:&T){b1.draw();b2.draw();} //+的第一个形式:在参数中书写 fn draw_ojbect3(shape:&(impl Brush+Display)){ shape.draw(); } //+的第二个形式:在方法名后书写 fn draw_object4<T:Brush+Display>(shape:&T){ shape.draw(); } //+的第三个形式:使用where字句 fn draw_ojbect31<T>(shape:&T) where T:Display+Brush { shape.draw(); } fn main() { let b1 = Box{vol: 20}; let t1 = tube{x:5, y:3, z:4, vol: 60}; let t2= tube{x:10,y:8,z:3,vol:210}; draw_objet(&b1,&t1); //draw_objet2(&b1,&t1);//这样会报错的,因为draw_objet2函数要求参数类型必须一致 draw_objet2(&t1,&t2); // //draw_ojbect3(&b1); //这个明显是错误的,因为要求,因为drawo_ojbect3要求参数必须实现两个接口特质 draw_ojbect3(&t1); draw_object4(&t1); draw_ojbect31(&t2); }
四、返回接口特质
和其它语言类似,但是这个有个问题:rust编译器只把返回的当作接口特质,而不是当作具体的对象(结构体或者枚举)。
所以,如果没有类似其它语言的强制转换,或者编译器支持,企图把返回的接口特质当作某个对象,那是不行的。
fn create_shape(px:u32,py:u32,pz:u32)->impl Brush{ Tube{ x:px, y:py, z:pz, vol:px*py*pz } } let my_tube=create_shape(10,20,40);
//println!("{}",my_tube.vol); // 这样会报错,因为编译器无法指导my_tube具体类型 my_tube.draw(); //但这个可以的。 所以能不能知道,全看编译器或者是rust发明人的意愿了。
上例中,为什么my_tube.vol会报告异常,是因为rust编译器的逻辑只认为my_tube是一个接口特质,所以my_tube不能调用Tube的属性/方法,但是
my_tube可以调用Brush的具体方法。
五、帮助实现通用类型函数/方法
前面的一堆内容就是为了两个目的:如何定义和实现接口特质;如何在通用类型方法中限定参数的范围。
“如何定义和实现”基本上都明白了,现在就示例下如何实现“通用类型参数限定范围”。
方式是:利用接口特质
语法:<P:T> 或者<P:T1+T2+..Tn>,或者也可以使用where字句
fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest }
六、小结
- 接口特质是特质的一个部分,不能把特质翻译为接口
- 定义接口特质还是比较简单的。可以在一个接口特质中定义多个方法。
- 不能在当前单元包中实现在其它单元包中定义的接口特质。但是存在例外情况,例如一些rust位于标准库中的接口特质;一个单元包内,不能在不同模块为一个接口特质,一个对象做n次实现
- 接口特质的方法可以有默认实现;默认实现的方法可以被覆盖
- 在方法中可以使用接口特质作为参数。有两种方式:impl,标准。前者不会强制所有参数同个类型,后者会
- 还可以使用where字句来限定参数的接口特质。
- 某个参数如果要绑定(限定)多个接口,可以使用+符号
- 接口特质的出现,使得定义参数,方法变得更加灵活,也更加复杂
- 函数/方法可以返回接口特质,但如果没有特别措施,不能把返回的结果当作具体类型使用。编译器只会把返回结果当值接口特质,即使你在方法体中明确返回的类型。