rust之trait

rust May 10, 2021

前言

trait中文翻译为特质,特征,特点。就类似于某个人身上的一些特别的东西,我们都可以把归类成一个人的特点。许多人都具有的类似的特点,我们可以泛化成一类特点。在rust中trait也是类似的东西,任何类型都可以实现一个trait。甚至我们可以给基本类型添加我们自己的trait。我们还可以给外部依赖的struct添加我们自己的trait。我理解这是一种类似duck type的思想,objec的行为不由type定义,而是由使用的method和property定义[1]。这在java中是没办法实现的,java中类似的东西应该就是接口了(interface)。
另外说一下,rust中没有继承机制,我觉得挺好的。为什么呢?我觉得是大家这些年对继承机制的思考得来的。在传统的面向对象的语言中,java或者cpp,继承导致的问题很多。无论是语法层面还是使用方面,cpp的菱形继承,继承的权限设计...java后面就否决了多继承。我们在使用的时候也会发现继承带来的冗余的属性和方法,多层继承体系的修改需要添加无用中间层。如果没有良好的设计超过三层的继承会导致认知上的困难。
这时候我们开始期望组合而不是继承,多个功能类(接口或者逻辑层面类似的东西)的组合可以带来更灵活的业务扩展和修改。良好的抽象建模把作用域限定在某个范围,理解起来很容易。
trait就是如此:A trait is a collection of methods defined for an unknown type: Self. 他是一系列方法的集合。使用起来很灵活,下面我们详细来写下。

定义

A type’s behavior consists of the methods we can call on that type. Different types share the same behavior if we can call the same methods on all of those types. Trait definitions are a way to group method signatures together to define a set of behaviors necessary to accomplish some purpose

trait是一系列行为方法的集合类型。不同的类型之间可以共享这些相同的方法。trait的定义是一种方式,让我们可以将众多方法打上同一个标签,让他们一起来实现某种目的。很绕,不过大概是这么个意思。用起来就好了,trait就是定义数据结构共有方法和行为的一个东西。因为trait有默认的实现,同时还可以在每个类型定义自己的实现。通过这种方式我们可以实现,多态和继承行为。

实现

我们通过官网的一个例子来学习trait。我们定义一个trait叫做summary,来显示不同类型数据的概述。


pub trait Summary {  // 定义trait的关键字 trait。
    fn summarize(&self) -> String;  // 没有默认实现,只是声明一个方法,没有方法体部分。这是一个方法,不是
                                    // 函数,因为入参是self。
}

给一个类型实现trait, 我们定义了一个新的类型——NewsArticle,然后让它实现Summary。在summarize里面输出了他的标题,作者和位置。又实现了一个tweet的类型,同样也实现了Summary。

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 main() {
    let tweet = Tweet {
        username: "zzx-blog".to_string(),
        content: "Yeah, this is from zzx-blog".to_string(),
        reply: false,
        retweet: false };

    println!("I new tweet {}", tweet.summarize()); 
                            // I new tweet zzx-blog: Yeah, this is from zzx-blog
}


下面看看试试默认实现,有了默认实现后,对于一些不需要实现自定义summary的类型就可以直接使用默认的实现。


pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

trait作为参数

我们可以把trait作为参数传递,那么任何实现了这个trait的类型都可以作为参数。例如我们实现以下函数

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

trait bound

如果参数多了,那么每个都要写impl Summary是比较麻烦,我们可以使用一个简写的方式,类似范型的写法。trait bound是一种约束或者声明,表明我们的参数是这种类型的trait。


fn notify<T: Summary>(item1: &T, item2: &T){
    println!("Breaking news! {}, {}", item1.summarize(), item2.summarize());
}

trait bound中的+语法

如果我们需要一个既实现了Summary又实现了Dispay的类型的话要怎么弄。通过+把两个trait关联起来。

pub fn notify<T: Summary + Display>(item: &T) {
  println!("Breaking news! {}", item.summarize());
}

trait bound中的where语法

假如我们又下面一个函数,我们发现trait bound的声明太长了,把前面都占用了。有一种更清晰的声明的方式。where语法声明。

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

fn some_function<T, U>(item1: &T, item2: &U) -> i32
    where T: Display + Clone, U: Summary + Display
{
    todo!()
}

将trait作为结果返回

如果我们期望类似java工厂方法那样返回interface,那么在rust里面是做不到的。下面这段代码会编译错误

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

error: `if` and `else` have incompatible types 

为什么会这样,因为rust没有继承系统,两个类型他们就是不一样的。如果有继承系统,让他们继承同一个父类,然后返回父类就可以了。
[me]:后来了解到主要是trait是要在编译器实现的,所以那两个返回的就是不同的类型,虽然他们实现了同一个trait的,但是经过编译器实现后,他们就是不同的类型了。但是通过box包裹,他们就是一个指向Summary的指针类型。
怎么办呢?使用box——智能指针将trait包裹起来。如下所示:

 
fn returns_summarizable(switch: bool) -> Box<dyn Summary> {
    if switch {
        Box::new(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 {
        Box::new(Tweet {
            username: String::from("horse_ebooks"),
            content: String::from(
                "of course, as you probably already know, people",
            ),
            reply: false,
            retweet: false,
        })
    }


总结

rust的声明语法太多了。如果一个方法/函数上面既有范型,有生命周期声明,又有trait声明,那对现在的我需要看一阵子才能搞清楚...

ref

  1. duck type
  2. trait example
  3. trait doc
  4. return trait object