Rust的Trait(定义共同行为)

trait定义了某个类型可能与其他类型共享的功能,用抽象的方式定义共同行为。

定义trait🔗

trait定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的合集。

例如,这里有存放了不同类型和属性文本的结构体NewsArticle和Tweet,我们想创建一个aggregator的库用来显示NewsArticle和Tweet的数据摘要,就可以调用summarize方法来请求摘要:

pub trait Summary {
    fn summarize(&self) -> String;
}

这里使用trait关键字来声明一个trait,在大括号里声明实现这个trait的类型所需要的行为的方法签名。

每一个实现这个trait的类型需要提供自定义的方法体。

为类型实现trait🔗

考虑以下代码:

pub struct NewArticle {
    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关键字,最后是类型名。现在NewsArticle和Tweet都实现了Summarytrait,我们可以像调用常规方法一样调用summarize方法,区别在于trait需要和类型一起引入作用域:

use aggregator::{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至少有一个属于当前crate时,我们才能对类型实现trait。这个限制是被称作相关性(coherence)的程序属性的一部分,或者更具体说是孤儿规则(orphan rule)。这个规则确保其他人编写的代码不会破坏你的代码。

默认实现🔗

为trait中的某些方法提供默认行为而不是每个类型每个实现都定义自己的行为很有用,当某个特定类型实现trait时,可以选择保留或重载每个方法的默认行为。

考虑如下代码:

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

如果想对NewsArticle使用这个默认实现,可以通过impl Summary for NewsArticle {}指定一个空的impl块。这个时候调用NewsArticle的summarize方法就会返回默认实现的结果。

默认实现允许调用相同trait中的其他方法,只需调用的方法定义了就行。

trait作为参数🔗

考虑下面代码:

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

对于item参数,我们指定了impl Summary而不是具体类型,这个参数支持任何实现了Summarytrait的类型。在notify函数中,我们可以调用任何来自Summary的方法。

trait bound语法🔗

impl Trait语法实际上是trait bound语法的语法糖:

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

impl Trait用于短小的例子,而trait bound用于更复杂的情况,若是参数为两个实现了Summarytrait的类型,我们可以这样写:

pub fn notify(item1: &impl Summary, item2: &impl Summary) {}

其中item1和item2的类型可以不同,如果需要它们都是相同类型,则需要这样写:

pub fn notify<T: Summary>(item1: &T, item2: &T) {}

这里item1和item2的类型都为T,所以类型必须相同。

指定多个trait bound🔗

如果notify函数需要显示itme的格式化形式,也要使用summarize方法,item就需要实现两个trait:

pub fn notify(item: &(impl Summary + Display)) {}

泛型的trait bound语法可以这样写:

pub fn notify<T: Summary + Display>(item: &T) {}

通过where简化trait bound🔗

泛型参数多的时候,代码难以阅读:

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

可以使用where子句来简化:

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

返回实现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,
    }
}

通过使用impl Summary作为返回值,我们可以返回任何实现了Summarytrait的类型,这里返回的是Tweet类型。

不过这只适用于返回一种类型,若使返回两种就不行:

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

这里尝试返回NewsArticle或Tweet类型,编译器会报错。

使用trait bound有条件地实现方法🔗

使用带有trait bound的impl块可以为实现了特定trait的类型实现方法:

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

这里cmp_display方法只对实现了Display和PartialOrdtrait的类型有效。同理也可以对实现了特定trait的类型实现trait,称作blanket implementation,例如:

impl<T: Display> ToString for T {
    // --snip--
}

标准库有这些blanket implementation,我们可以对任何实现了Displaytrait的类型使用ToString里面的方法。