34_trait

803 阅读7分钟

1. 概述

Trait告诉Rust编译器,某种类型具有哪些并且可以与其他类型共享的功能,可以使用抽象的方式定义共享行为。Trait bounds(约束)指的是泛型类型参数指定实现了特定行为的类型。

Trait与其他语言的借口(interface)类似,但是有区别。

2. 定义Trait

定义Trait的目的是把方法签名放在一起,来实现某种目的所必须的一组行为。定义trait的相关规则如下

  • 关键字:trait
  • 只有方法签名,没有具体实现
  • trait可以有多个方法,每个方法占一行,以;结尾
  • 实现该trait的类型必须提供具体方法的实现

如下定义trait的示例代码

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

3. 在类型上实现trait

与类型的实现方法类似,如下示例代码

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

pub struct NewArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewArticle {
    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: 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());
}

4. 实现某个trait的约束

可以在某个类型上实现某个trait的前提条件为:这个类型或这个trait在本地crate里定义的。

无法为外部类型来实现外部的trait,这个限制是程序属性的一部分(也就是一致性),更具体地说是孤儿原则,之所以这样命名是因为父类型不存在。此规则确保他人的代码不能破坏你写的代码,反之亦然。如果没有这个规则,两个crate可以为同一个类型实现同一个trait,rust就不知道应该使用哪个实现了。

5. 默认实现

trait可以拥有默认实现,当我们为某个类型实现trait时,可以选择重载或者保留原有的默认实现。默认实现的方法也可以调用trait中其他的方法,即使这些方法没有默认实现。如下示例代码

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

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct NewArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewArticle {
    fn summarize_author(&self) -> String {
        format!("@{} ", self.author)
    }
}

需要注意的是,我们无法在方法的重写实现里调用默认的实现。

6. 使用trait作为参数

6.1 impl Trait

使用impl Trait的语法,可以适用于简单的函数传参情况。加入我们希望传入的参数实现了一个Summary这个trait,那么我们可以使用如下的参数声明方式

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

那么我们在调用此函数的使用,可以传入Tweet,也可以传入NewArticle

6.2 Trait bound

使用Trait bound可以与复杂的情况,如下示例代码

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

如果包含两个参数,使用Trait bound的写法如下

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

实际上impl TraitTrait bound的语法糖。

6.3 使用+指定多个trait bound

如果使用impl Trait的方式,示例代码如下

use std::fmt::Display;

pub fn notify(item: impl Summary + Display) {
    println("breaking news! {}", item.summarize());
}

使用Trait bound的写法如下

use std::fmt::Display;

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

6.4 在方法签名后面使用where子句

如果一个函数要求的变量需要实现过多的Trait,代码的可读性会变低,很不直观,如下示例代码

pub fn notify<T: Summary + Display, U: Clone + Debug>(a: T, b: U) -> String {
    format!("breaking news! {}", a.summarize());
}

我们可以使用where子句还简化Trait的约束,如下示例代码

pub fn notify<T, U>(a: T, b: U) -> String
where T: Summary + Display,
    U: Clone + Debug,
{
    format!("breaking news! {}", a.summarize());
}

7. 使用trait作为返回值类型

使用impl Trait声明返回的类型为Trait,如下示例代码

pub fn notify(flag: bool) -> impl Summary {
    // ......
}

使用impl Trait只能返回确定的同一种类型,返回可能不同类型的代码会报错,如下代码将报错

pub fn notify(flag: bool) -> impl Summary {
    if flag {
        NewArticle {
            // ......
        }
    } else {
        Tweet {
            // ......
        }
    }
}

8. 使用PartialOrd实现数据比较

如下示例代码

use std::result;

fn largest<T: PartialOrd +Clone>(list: &[T]) -> &T {
    let mut largest = &list[0];

    for item in list.iter() {
        if item > &largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let str_list = vec![String::from("hello"), String::from("world")];
    let result = largest(&str_list);
    println!("The largest word id {}", result);
}

这段代码定义了一个泛型函数largest,该函数接受一个类型为[T]的切片参数list,并返回切片中最大的元素的引用。

在函数定义中,我们为泛型参数T添加了两个trait bound,PartialOrd和Clone。PartialOrd trait指定了T类型必须支持比较操作符(如>, <等),而Clone trait指定T类型必须支持克隆操作。

在函数体中,我们首先将largest初始化为切片list中的第一个元素的引用。然后,我们遍历切片中的每个元素,并与largest进行比较。

注意,我们使用了&largest,因为largest是一个引用,而item是一个值。通过使用&,我们可以将item转换为一个引用进行比较。

如果item大于largest,我们更新largest为item。最后,我们返回largest。

由于largest函数使用了PartialOrd和Clone trait,它可以用于任意支持比较和克隆的类型。在这种情况下,我们使用了String类型,因为它满足这些要求。但是,如果我们传递了一个不支持比较或克隆的类型的切片,代码将无法编译。

9. 使用Trait Bound有条件的实现方法

在使用泛型类型参数的 impl 块上使用Trait bound,我们可以有条件的为了实现特定 trait 的类型来实现。如下示例代码

use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

// 所有Pair<T> 都拥有new函数
impl <T> Pair<T>{
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

// 实现了Display 和 PartialOrd 的Pair<T> 猜拥有com_display函数
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);
        }
    }
}

fn main(){

}

也可以为实现其他Trait的类型有条件的实现某个Trait。为满足Trait Bound的所有类型上实现Trait叫做覆盖实现(Blanket implementations)。

10. 总结

Trait是Rust中的一种抽象机制,用于定义共享行为并与类型进行交互。Trait可以看作是一组方法签名的集合,它定义了类型必须实现的方法。

Trait的用法包括以下几个方面:

  1. 定义Trait:使用trait关键字定义Trait,指定方法的签名。Trait中的方法没有具体的实现,只有方法签名。

  2. 实现Trait:使用impl关键字在类型上实现Trait,提供方法的具体实现。实现Trait时,方法名和参数列表必须与Trait定义中的方法签名一致。

  3. 默认实现:Trait可以具有默认实现,当为类型实现Trait时,可以选择重写Trait中的方法或保留默认实现。

  4. Trait作为函数参数:函数可以接受Trait作为参数类型,这样函数可以处理不同的类型,只要这些类型实现了所需的Trait。

  5. Trait作为返回值类型:函数可以返回Trait作为返回值类型,这样函数可以根据具体情况返回不同的实现Trait的类型。

  6. Trait作为泛型约束:通过Trait约束,可以限制泛型类型参数必须实现特定的Trait,从而在编译时保证类型的一致性。

  7. 多个Trait约束:可以在泛型约束中指定多个Trait,使用+来连接不同的Trait。

  8. Trait的继承:一个Trait可以继承另一个Trait,使用trait SubTrait: SuperTrait的语法来指定Trait的继承关系。

Trait的使用可以提高代码的可读性和重用性,使得代码更加模块化和可扩展。通过定义和实现Trait,可以定义通用的行为,使得代码更具有灵活性和可维护性。