进阶篇-Trait(特性)

  • 木头
  • 更新于 2023-03-03 15:25
  • 阅读 1998

trait是rust非常重要的知识点

trait用于定义与其它类型共享的功能,类似于其它语言中的接口。

  1. 可以通过 trait 以一种抽象的方式定义共享的行为。
  2. 可以使用 trait bounds 指定泛型是任何拥有特定行为的类型。

定义 trait

一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。

例如,老师和学校之间他们有的共同行为是都有自己的名称和年龄,不同行为例如老师有教的科目和学生所在的年级:

pub trait GetInformation {
    fn get_name(&self) -> &String;
    fn get_age(&self) -> &u32;
}

这里使用 trait 关键字来声明一个trait,后面是trait的名字,在这个例子中是 GetInformation。我们也声明traitpub 以便依赖这个 cratecrate 也可以使用这个 trait。在大括号中声明描述实现这个trait的类型所需要的行为的方法签名,在这个例子中是 get_name,get_agetrait里面的函数可以没有函数体,实现代码交给具体实现它的类型去补充。trait体中可以有多个方法:一行一个方法签名且都以分号结尾。

为类型实现 trait

如果类型实现这个trait 的类型就必须提供其自定义行为的方法体,否则编译不通过:

pub trait GetInformation {
    fn get_name(&self) -> &String;
    fn get_age(&self) -> u32;
}

// 老师
pub struct Teacher {
    name: String,
    age: u32,
    subject: String,
}

impl GetInformation for Teacher {
    fn get_name(&self) -> &String {
        &self.name
    }
    fn get_age(&self) -> u32 {
        self.age
    }
}

impl Teacher {
    fn get_subject(&self) -> &String {
        &self.subject
    }
}

// 学生
pub struct Student {
    name: String,
    age: u32,
    grade: String,
}

impl GetInformation for Student {
    fn get_name(&self) -> &String {
        &self.name
    }
    fn get_age(&self) -> u32 {
        self.age
    }
}

impl Student {
    fn get_grade(&self) -> &String {
        &self.grade
    }
}

fn main() {
    let s = Student {
        name: String::from("李四"),
        age: 9,
        grade: String::from("六年级"),
    };

    println!(
        "学生 name = {},age = {},grade = {}",
        s.get_name(),
        s.get_age(),
        s.get_grade()
    );

    let t = Teacher {
        name: String::from("罗翔"),
        age: 36,
        subject: String::from("律师"),
    };
    println!(
        "老师 name = {},age = {},subject = {}",
        t.get_name(),
        t.get_age(),
        t.get_subject()
    );
}

在类型上实现 trait类似于实现与 trait 无关的方法。区别在于 impl 关键字之后,我们提供需要实现 trait 的名称,接着是for和需要实现 trait的类型的名称。在 impl块中,使用trait定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现trait方法所拥有的行为。

默认实现

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

pub trait GetInformation {
    fn get_name(&self) -> &String;
    fn get_age(&self) -> u32;
    fn get_complete(&self) -> String {
        format!(
            "name = {},age = {},school = {}",
            self.get_name(),
            self.get_age(),
            String::from("北京大学")
        )
    }
}

// 老师
pub struct Teacher {
    name: String,
    age: u32,
    subject: String,
}

impl GetInformation for Teacher {
    fn get_name(&self) -> &String {
        &self.name
    }
    fn get_age(&self) -> u32 {
        self.age
    }
}

impl Teacher {
    fn get_subject(&self) -> &String {
        &self.subject
    }
}

// 学生
pub struct Student {
    name: String,
    age: u32,
    grade: String,
}

impl GetInformation for Student {
    fn get_name(&self) -> &String {
        &self.name
    }
    fn get_age(&self) -> u32 {
        self.age
    }
}

impl Student {
    fn get_grade(&self) -> &String {
        &self.grade
    }
}

fn main() {
    let s = Student {
        name: String::from("李四"),
        age: 9,
        grade: String::from("六年级"),
    };

    println!("{}", s.get_complete());

    let t = Teacher {
        name: String::from("罗翔"),
        age: 36,
        subject: String::from("律师"),
    };
    println!("{}", t.get_complete());
}

如果想要对GetInformation 实例使用这个默认实现,可以通过 impl GetInformation for Teacher {}指定一个空的impl块,如果已经存在impl块可以不用实现默认方法。

默认实现允许调用相同 trait中的其他方法,哪怕这些方法没有默认实现。如此,trait 可以提供很多有用的功能而只需要实现指定一小部分内容。

trait 作为参数

知道了如何定义trait 和在类型上实现这些 trait 之后,我们可以探索一下如何使用 trait来作为参数:

......
fn print_information(itme: impl GetInformation) {
    println!("{}", itme.get_complete())
}

fn main() {
    let s = Student {
        name: String::from("李四"),
        age: 9,
        grade: String::from("六年级"),
    };

    print_information(s);

    let t = Teacher {
        name: String::from("罗翔"),
        age: 36,
        subject: String::from("律师"),
    };
    print_information(t);
}

对于item 参数,我们指定了 impl关键字和 trait 名称,而不是具体的类型。该参数支持任何实现了指定 trait 的类型。在print_information 函数体中,可以调用任何来自 GetInformation trait 的方法,比如 print_information。我们可以传递任何 StudentTeacher 的实例来调用 print_information。任何用其它如 Stringi32 的类型调用该函数的代码都不能编译,因为它们没有实现 GetInformation

Trait Bound 语法

impl Trait 语法适用于直观的例子,它实际上是一种较长形式语法的语法糖。我们称为 trait bound,它看起来像:

fn print_information<T: GetInformation>(itme: T) {
    println!("{}", itme.get_complete())
}

这与之前的例子相同,不过稍微冗长了一些。trait bound 与泛型参数声明在一起,位于尖括号中的冒号后面。

impl Trait 很方便,适用于短小的例子。更长的 trait bound 则适用于更复杂的场景。例如,可以获取两个实现了 GetInformation 的参数。使用 impl Trait 的语法看起来像这样:

......
fn print_information<T: GetInformation>(itme1: T, itme2: T) {
    println!("{}", itme1.get_complete());
    println!("{}", itme2.get_complete());
}

fn main() {
    let s1 = Student {
        name: String::from("李四1"),
        age: 9,
        grade: String::from("六年级"),
    };
    let s2 = Student {
        name: String::from("李四2"),
        age: 9,
        grade: String::from("六年级"),
    };

    print_information(s1, s2)
}

泛型 T 被指定为 item1item2的参数限制,如此传递给参数 item1item2值的具体类型必须一致。

通过 + 指定多个 trait bound

如果 print_information 需要 itemGetName,同时也要使用 GetAge 方法,那么 item 就需要同时实现两个不同的 trait:GetNameGetAge。这可以通过+ 语法实现:

pub trait GetName {
    fn get_name(&self) -> &String;
}
pub trait GetAge {
    fn get_age(&self) -> u32;
}

// 老师
pub struct Teacher {
    name: String,
    age: u32,
    subject: String,
}

impl GetName for Teacher {
    fn get_name(&self) -> &String {
        &self.name
    }
}
impl GetAge for Teacher {
    fn get_age(&self) -> u32 {
        self.age
    }
}

fn print_information<T: GetName + GetAge>(itme: T) {
    println!("{}", itme.get_name());
    println!("{}", itme.get_age());
}

fn main() {
    let t = Teacher {
        name: String::from("罗翔"),
        age: 36,
        subject: String::from("律师"),
    };
    print_information(t);
}

Teacher必须实现GetNameGetAgetrait,否则将无法调用print_information函数,<T: GetName + GetAge>更像调用的条件。

通过 where 简化 trait bound

然而,使用过多的trait bound 也有缺点。每个泛型有其自己的 trait bound,所以有多个泛型参数的函数在名称和参数列表之间会有很长的 trait bound 信息,这使得函数签名难以阅读。为此,Rust 有另一个在函数签名之后的 where从句中指定 trait bound的语法。所以除了这么写:

fn print_information<T: GetName, U: GetAge>(itme1: T, itme2: U) {
    println!("{}", itme1.get_name());
    println!("{}", itme2.get_age());
}

还可以像这样使用 where从句:

fn print_information<T, U>(itme1: T, itme2: U)
where
    T: GetName,
    U: GetAge,
{
    println!("{}", itme1.get_name());
    println!("{}", itme2.get_age());
}

这个函数签名就显得不那么杂乱,函数名、参数列表和返回值类型都离得很近,看起来跟没有那么多 trait bounds 的函数很像。

返回实现了 trait 的类型

也可以在返回值中使用impl Trait语法,来返回实现了某个 trait 的类型:

fn produce_item_with_age() -> impl GetAge {
    Teacher {
        name: String::from("罗翔"),
        age: 36,
        subject: String::from("律师"),
    }
}

通过使用impl GetAge作为返回值类型,我们指定了 produce_item_with_age 函数返回某个实现了GetAge trait 的类型,但是不确定其具体的类型。在这个例子中 produce_item_with_age返回了一个 Teacher,不过调用方并不知情。

不过这只适用于返回单一类型的情况。例如,这段代码的返回值类型指定为返回 impl GetAge,但是返回了 TeacherStudent 就行不通:

pub trait GetName {
    fn get_name(&self) -> &String;
}
pub trait GetAge {
    fn get_age(&self) -> u32;
}

// 老师
pub struct Teacher {
    name: String,
    age: u32,
    subject: String,
}

impl GetAge for Teacher {
    fn get_age(&self) -> u32 {
        self.age
    }
}

// 学生
pub struct Student {
    name: String,
    age: u32,
    grade: String,
}
impl GetAge for Student {
    fn get_age(&self) -> u32 {
        self.age
    }
}

fn produce_item_with_age(b: bool) -> impl GetAge {
    if b {
        Teacher {
            name: String::from("罗翔"),
            age: 36,
            subject: String::from("律师"),
        }
    } else {
        Student {
            name: String::from("李四"),
            age: 9,
            grade: String::from("六年级"),
        }
    }
}

fn main() {
    let t = produce_item_with_age();
}

这里尝试返回TeacherStudent。这不能编译,因为 impl Trait 工作方式的限制。

泛型类型遗留问题

在泛型类型章节我们做过一个功能就是寻找 slice 中最大值:

fn largest<T>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

fn main() {
    let number_list = vec![11, 232, 63, 74, 51, 96];
    let result = largest(&number_list);
    println!("最大的数字是:{}", result);

    let char_list = vec!['a', 't', 'k', 's', 'x', 'l'];
    let result = largest(&char_list);
    println!("最大的字符是:{}", result);
}

largest 函数在它的签名中使用了泛型,统一了两个实现,这个时候我们需要给泛型T加上trait特性限制:

fn largest<T>(list: &[T]) -> T
where
    T: PartialOrd + Copy,
{
    let mut largest = list[0];
    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }
    largest
}

这里我们限制T必须实现比较和复制的trait

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

通过使用带有trait bound的泛型参数的 impl 块,可以有条件地只为那些实现了特定 trait 的类型实现方法:

trait GetName {
    fn get_name(&self) -> &String;
}

trait GetSubject {
    fn get_subject(&self) -> &String;
}

trait GetGrade {
    fn get_grade(&self) -> &String;
}

// 学校
struct School<T, S> {
    teacher: T,
    student: S,
}

impl<T, S> School<T, S>
where
    T: GetName + GetSubject,
    S: GetName + GetGrade,
{
    fn print_school(&self) {
        println!("{}-{}", self.teacher.get_name(), self.teacher.get_subject());
        println!("{}-{}", self.student.get_name(), self.student.get_grade());
    }
}

// 老师
pub struct Teacher {
    name: String,
    age: u32,
    subject: String,
}

impl GetName for Teacher {
    fn get_name(&self) -> &String {
        &self.name
    }
}
impl GetSubject for Teacher {
    fn get_subject(&self) -> &String {
        &self.subject
    }
}

// 学生
pub struct Student {
    name: String,
    age: u32,
    grade: String,
}
impl GetName for Student {
    fn get_name(&self) -> &String {
        &self.name
    }
}
impl GetGrade for Student {
    fn get_grade(&self) -> &String {
        &self.grade
    }
}

fn main() {
    let s = Student {
        name: String::from("李四"),
        age: 9,
        grade: String::from("六年级"),
    };

    let t = Teacher {
        name: String::from("罗翔"),
        age: 36,
        subject: String::from("律师"),
    };
    let school = School {
        teacher: t,
        student: s,
    };
    school.print_school();
}

School结构体的print_school方法的特性是T必须实现GetNameGetSubjectU必须实现GetNameGetGradetrait才能调用print_school方法。

因为我们向编译器提供了 trait bound 信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中,如果我们尝试调用一个类型并没有实现的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复错误。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了,这样相比其他那些不愿放弃泛型灵活性的语言有更好的性能。

  • 原创
  • 学分: 4
  • 分类: Rust
  • 标签:
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。
125 订阅 31 篇文章

0 条评论

请先 登录 后评论
木头
木头
0xC020...10cf
江湖只有他的大名,没有他的介绍。