Rust 中的 trait 抽象和约束

1,495 阅读7分钟

trait 类似于其他编程语言中的常被称为接口(interface)的功能,但还是有一些区别的。 trait 告诉 Rust 编译器某个特定类型拥有可能与其他类型共享的功能。可以通过 trait 以一种抽象的方式定义共享的行为。可以使用 trait bounds 指定泛型是任何拥有特定行为的类型。

简单的理解,trait 就是 Rust 中的接口,定义了某个类型使用这个接口时的行为。使用 trait 可以约束多个类型之间共享的行为,在使用泛型编程时还能限制泛型必须符合 trait 规定的行为。

定义 trait

如果不同的类型具有相同的行为,那么就可以定义一个 trait,然后为这些类型实现该 trait。定义 trait 就是把一些方法组合在一起,目的是定义一个实现某些目标所必需的行为和集合。

trait 是定义了一系列方法的接口:

pub trait Summary {
    // trait 里面的方法只需要写声明即可
    fn summarize_author(&self) -> String;
    // 定义为默认实现的方法,其他类型就无需再实现该方法
    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

  • 定义了一个名称为Summary的 trait,它包含summarize_authorsummarize两个方法提供的行为。
  • trait 里面的方法只需要写上声明即可,实现交给具体的结构体来做,当然,方法也可以有默认实现的,这里的summarize方法就包含默认实现,并且这里在summarize方法默认实现内部还调用了不包含默认实现的 summarize_author方法。
  • Summary trait的两个方法的参数中都包含关键字self,与结构体方法一样,self 用作 trait 方法的第一个参数。

注:实际上selfself: Self的简写,&selfself: &Self的简写,&mut selfself &mut Self的简写。 Self代表的是当前实现了 trait 的类型,例如有一个类型 Foo 实现了 Summary trait,则实现方法时中的 Self 就是Foo。

pub trait Summary {

    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("@{}发表了帖子...", self.summarize_author())
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

pub struct Post { 
    pub title: String, // 标题 
    pub author: String, // 作者
    pub content: String, // 内容 
}

impl Summary for Post {
   fn summarize_author(&self) -> String { 
     format!("{}发表了贴子", self.author)
   } 
   fn summarize(&self) -> String { 
     format!("{}发表了贴子:{}", self.author, self.content)
   } 
} 
impl Summary for Tweet { 
   fn summarize_author(&self) -> String { 
     format!("{}发表了微博", self.username)
   } 
   fn summarize(&self) -> String { 
     format!("@{}发表了微博:{}", self.username, self.content)
   } 
}

fn main() {
   let tweet = Tweet {
       username: String::from("haha"),
       content: String::from("the content"),
       reply: false,
       retweet: false,
   };
   println!("{}",  tweet.summarize())
}

关于 trait 实现与定义的位置,有一条非常重要的原则(孤儿原则): 如果你想要为类型 A 实现trait T,那么 A 或者 T 至少有一个是在当前作用域中定义的!

这个规则可以确保其他人编写的代码不会破坏你的代码,也确保了你不会莫名其妙破坏别人的代码。

使用 trait 作为函数参数

Trait 可以用作函数参数,这里先定义一个函数,使用 trait 用作函数参数:

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

参数的意思就是 实现了 Summary trait 的 item 参数。可以使用任何实现了 Summary trait 的类型作为该函数的参数,同时在函数体内,还可以调用该 trait 的方法。

Trait 约束 (trait bound)

上面使用的 impl Trait 实际上只是一个语法糖,其完整书写形式如下,形如 T: Summary 被称为 trait 约束。

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

对于比较复杂的使用场景,特征约束可以让我们拥有更大的灵活性和语法表现能力,例如一个函数接受两个 impl Summary 的参数:

pub fn notify(item1: &impl Summary, item2: &impl Summary) {} // trait 参数
pub fn notify<T: Summary>(item1: &T, item2: &T) {}  // 泛型 T 约束:说明 item1 和 item2 必须拥有同样的类型,同时说明 T 必须实现 Summary trait

通过 + 指定多个 trait bound

除了单个约束条件,还可以指定多个约束条件,例如让参数实现多个 trait:

pub fn notify(item: &(impl Summary + Display)) {}  // 语法糖形式
pub fn notify<T: Summary + Display> (item: &T) {}  // trait bound 完整形式

通过 where 简化 trait bound

当特征约束变得很多时,函数的签名就会变得很复杂,这时如果使用 where 可以对其做一些形式上的改进:

// 当出现多个泛型类型时,过多的trait bound会导致函数签名难以阅读
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 { ... }
// 使用 where 做简化,使得函数名、参数列表和返回值类型都离得很近,看起来跟没有那么多 trait bounds 的函数很像
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
      U: Clone + Debug {
      ....
}

使用 trait 约束有条件地实现方法或 trait

trait 约束作为参数,可以让我们在指定类型+指定 trait 的条件下实现方法,使函数能接受来自多个不同类型的参数。 例如:

fn notify(summary: impl Summary) {
    println!("notify: {}",  summary.summarize())
}

fn notify_all(summaries: Vec<impl Summary>) {
    for summary in summaries {
        println!("notify: {}",  summary.summarize())
    }
}

fn main() {
   let tweet = Weibo {
       username: String::from("haha"),
       content: String::from("the content"),
       reply: false,
       retweet: false,
   };
   let tweets = vec![tweet];
   notify_all(tweets);
}

函数的summary参数的类型是impl Summary,而不是具体的类型。这样该参数就支持任何实现了Summary trait的类型。

当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候,可以使用智能指针 Box 和关键字 dyn 组合的 trait 对象(注:后续将单独章节学习 trait 对象)。

fn notify(summary: Box<dyn Summary>) {
    println!("notify: {}",  summary.summarize())
}

fn notify_all(summaries: Vec<Box<dyn Summary>>) {
    for summary in summaries {
        println!("notify: {}",  summary.summarize())
    }
}

fn main() {
   let tweet = Tweet {
       username: String::from("haha"),
       content: String::from("the content"),
       reply: false,
       retweet: false,
   };
   let tweets: Vec<Box<dyn Summary>> = vec![Box::new(tweet)];
   notify_all(tweets);
}

在泛型中使用trait

最后来看一下在泛型编程中使用 trait 限制泛型类型的行为。

上面定义 trait 例子中 notify 函数fn notify(summary: impl Summary),对于 summary 参数类型,指定的是 impl 关键字加 trait 名称,而不是具体的类型。 实际上impl Summary是泛型编程中 Trait Bound 的语法糖,所以上面的impl trait 代码可以改写为以下形式:

fn notify<T: Summary>(summary: T) {
    println!("notify: {}",  summary.summarize())
}

fn notify_all<T: Summary>(summaries: Vec<T>) {
    for summary in summaries {
        println!("notify: {}",  summary.summarize())
    }
}


fn main() {
   let tweet = Tweet {
       username: String::from("haha"),
       content: String::from("the content"),
       reply: false,
       retweet: false,
   };
   let tweets = vec![tweet];
   notify_all(tweets);
}

函数返回中的 impl Trait

可以通过 impl Trait 来说明一个函数返回了一个类型,该类型实现了某个 trait:

fn returns_summarizable() -> impl Summary {
    Tweet {
       username: String::from("haha"),
       content: String::from("the content"),
       reply: false,
       retweet: false,
   }
}

这种 impl Trait 形式的返回值只能是单一类型的 trait,如果多个类型的 trait 就会报错,比如:

fn returns_summarizable(switch: bool) -> impl Summary {  
    if switch {
        Tweet { ... }  // 此处不能返回两个不同的类型
    } else {
        Post { ... }  // 此处不能返回两个不同的类型
    }
}

上面的代码会报错,因为返回了两个不同的类型 trait: Tweet 和 Post。如果想要实现返回不同的类型,需要使用trait 对象,关于这部分后续将单独章节学习。

fn returns_summarizable(switch: bool) -> Box<dyn Summary> {  
    if switch {
        Box::new(Tweet { ... }) // trait 对象
    } else {
        Box::new(Post { ... })  // trait 对象
    }
}

总结

在 Rust 设计目标中,零成本抽象是非常重要的一条,它让 Rust 具备高级语言表达能力的同时,又不会带来性能损耗。零成本的基石是泛型与 trait,它们可以在编译期把高级语法编译成与高效的底层代码,从而实现运行时的高效。

trait 是以抽象的方式定义共享的行为,trait bound 是对函数参数或者返回值定义类型约束,比如impl SuperTraitT: SuperTrait,trait 和 trait bound 让我们使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。因为我们向编译器提供了 trait bound 信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为。综上,trait 用途简单的概括为:

  • 抽象行为:类似接口,是对类型共性的同一性抽象,定义共同行为
  • 类型约束:类型的行为被 trait 限定在更有限的范围内

参考