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语法:

trait Summary {
    fn summarize(&self) -> String;
    fn get_content(&self) -> &String;
    fn is_empty(&self) -> bool {
        self.get_content().is_empty()
    }
}
struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
    pub time: String
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}  -- {}", self.username, self.content,self.time)
    }
    fn get_content(&self) -> &String {
        &self.content
    }
}

fn main(){
    let tweet = Tweet {
        username: String::from("勒布朗.詹姆斯"),
        content: String::from("我,要退网!"),
        reply: false,
        retweet: false,
        time: String::from("2024-11-21 05:01:01")
    };
    println!("{}", tweet.summarize());
}

 几个注意事项:

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
    }
}

 

默认实现,减少了打码量,尤其是这个接口特质可能被许多对象实现的时候。

三、接口特质作为一个参数

这里讨论几个问题:

  1. 如何把一个接口特质作为一个参数,或者说代码上如何写?语法是什么
  2. 如果要求所有的参数实现同个接口特质,且是同种类型,要怎么写?
  3. 如果参数要求实现多个接口特质怎么定义。即某个参数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个:

  1. 使用+号,连接多个接口特质
  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
}

 

六、小结

  1. 接口特质是特质的一个部分,不能把特质翻译为接口
  2. 定义接口特质还是比较简单的。可以在一个接口特质中定义多个方法。
  3. 不能在当前单元包中实现在其它单元包中定义的接口特质。但是存在例外情况,例如一些rust位于标准库中的接口特质;一个单元包内,不能在不同模块为一个接口特质,一个对象做n次实现
  4. 接口特质的方法可以有默认实现;默认实现的方法可以被覆盖
  5. 在方法中可以使用接口特质作为参数。有两种方式:impl,标准。前者不会强制所有参数同个类型,后者会
  6. 还可以使用where字句来限定参数的接口特质。
  7. 某个参数如果要绑定(限定)多个接口,可以使用+符号
  8. 接口特质的出现,使得定义参数,方法变得更加灵活,也更加复杂
  9. 函数/方法可以返回接口特质,但如果没有特别措施,不能把返回的结果当作具体类型使用。编译器只会把返回结果当值接口特质,即使你在方法体中明确返回的类型。

 

posted @ 2024-11-23 11:52  正在战斗中  阅读(49)  评论(0编辑  收藏  举报