跳转到内容

Rust trait

trait(特征)定义了一组类型可以实现的行为,有点像其他语言里的接口(interface)。Rust 就是用它来表达“这个类型能做某件事”的,你在泛型那一页见到的 trait 约束,背后靠的也是它。

trait Speak {
fn speak(&self) -> String;
}

这段代码的意思是:任何实现了 Speak 的类型,都必须提供一个返回 String 的 speak 方法。trait 本身只是一个约定,在有类型实现它之前,它什么也不做。

trait Speak {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
impl Speak for Dog {
fn speak(&self) -> String {
String::from("汪汪!")
}
}
impl Speak for Cat {
fn speak(&self) -> String {
String::from("喵喵!")
}
}
fn main() {
let dog = Dog;
let cat = Cat;
println!("{}", dog.speak());
println!("{}", cat.speak());
}

输出:

汪汪!
喵喵!
▶ 动手试一试

Dog 和 Cat 是两个毫不相干的类型,但它们都实现了 Speak,所以都能调用 .speak()。这和结构体那一页的 impl 块是同一个套路:impl Speak for Dog 只是在说“Dog 是这样满足 Speak 这个 trait 的”。

真正的好处在这里:你可以写一个函数,接受任何实现了某个 trait 的类型,而不用关心它具体是哪个类型:

trait Speak {
fn speak(&self) -> String;
}
struct Dog;
impl Speak for Dog {
fn speak(&self) -> String { String::from("汪汪!") }
}
fn announce(animal: &impl Speak) {
println!("这只动物说:{}", animal.speak());
}
fn main() {
let dog = Dog;
announce(&dog);
}
▶ 动手试一试

animal: &impl Speak 的意思是:“任何实现了 Speak 的类型都行,我不需要知道具体是哪一个。”再加一个同样实现了 Speak 的 Bird 类型,announce 不用改一行代码就能直接用它。

还记得结构体那一页的 #[derive(Debug)] 吗?Debug 本身就是一个 trait,derive 只是替你自动生成了它的实现,省得你手写。你还会经常碰到这些:Clone(用于 .clone())、PartialEq(用于 ==)和 PartialOrd(用于 <、>,你在泛型那一页已经见过它被当作 trait 约束)。

trait 可以为方法提供一个默认的方法体,实现它的类型可以直接沿用,也可以覆盖(override)它:

trait Greet {
fn name(&self) -> String;
fn greet(&self) -> String {
format!("你好,{}!", self.name())
}
}

任何实现 Greet 的类型都只需要定义 name(),greet() 是白送的,除非它也想自己重写一个。

trait 的内容远不止这一页讲的这些。常用 trait那一页会带你过一遍每天都会用到的标准 trait,高级 trait则讲关联类型、supertrait(父 trait)等在大型代码库里常见的工具。

定义一个 trait Area,只有一个方法 fn area(&self) -> f64。为 Rectangle(有 width 和 height)和 Square(有 side)实现它。打印一个 3 × 4 的矩形和一个边长为 5 的正方形的面积。提示:每个 impl Area for ... 块只需要这一个方法,方法体就是一次乘法。

查看答案
trait Area {
fn area(&self) -> f64;
}
struct Rectangle {
width: f64,
height: f64,
}
struct Square {
side: f64,
}
impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
impl Area for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
fn main() {
let rect = Rectangle { width: 3.0, height: 4.0 };
let square = Square { side: 5.0 };
println!("矩形:{}", rect.area());
println!("正方形:{}", square.area());
}

输出:

矩形:12
正方形:25
▶ 动手试一试

定义一个 trait Describe,带有 fn describe(&self) -> String。为 Book(有 title)和 Movie(有 title 和 minutes)实现它。然后写一个函数 fn show(item: &impl Describe) 打印描述,并分别用这两种类型调用它。提示:用 format! 拼出每个描述字符串。

查看答案
trait Describe {
fn describe(&self) -> String;
}
struct Book {
title: String,
}
struct Movie {
title: String,
minutes: u32,
}
impl Describe for Book {
fn describe(&self) -> String {
format!("图书:《{}》", self.title)
}
}
impl Describe for Movie {
fn describe(&self) -> String {
format!("电影:《{}》({} 分钟)", self.title, self.minutes)
}
}
fn show(item: &impl Describe) {
println!("{}", item.describe());
}
fn main() {
let book = Book { title: String::from("霍比特人") };
let movie = Movie { title: String::from("飞屋环游记"), minutes: 96 };
show(&book);
show(&movie);
}

输出:

图书:《霍比特人》
电影:《飞屋环游记》(96 分钟)
▶ 动手试一试

拿这一页的 Greet trait 来用,它带有默认的 greet() 方法。为 Person 实现它,沿用默认实现;再为 Pirate 实现它,覆盖 greet(),改成海盗腔的 啊嗬,...!。提示:两个类型都必须定义 name(),只有 Pirate 还要自己写一个 greet()。

查看答案
trait Greet {
fn name(&self) -> String;
fn greet(&self) -> String {
format!("你好,{}!", self.name())
}
}
struct Person {
name: String,
}
struct Pirate {
name: String,
}
impl Greet for Person {
fn name(&self) -> String {
self.name.clone()
}
}
impl Greet for Pirate {
fn name(&self) -> String {
self.name.clone()
}
fn greet(&self) -> String {
format!("啊嗬,{}!", self.name())
}
}
fn main() {
let ana = Person { name: String::from("Ana") };
let jack = Pirate { name: String::from("Jack") };
println!("{}", ana.greet());
println!("{}", jack.greet());
}

输出:

你好,Ana!
啊嗬,Jack!
▶ 动手试一试
  • trait 定义了一个类型可以实现的行为,类似于接口。
  • impl TraitName for Type 提供具体的实现。
  • 把 &impl TraitName 用作参数类型,就能接受任何实现了这个 trait 的类型。
  • trait 可以提供默认的方法体,实现者可以沿用,也可以覆盖。
  • Debug、Clone 和 PartialEq 都是标准库里常见的 trait,#[derive(...)] 可以自动实现它们。

小测验

1. trait 定义的是什么?

2. 参数类型写成 animal: &impl Speak 是什么意思?

3. #[derive(Debug)] 实际上做了什么?

4. trait 中的默认实现是什么?

5. 根据这一页,下面哪个是 trait,而不是结构体或枚举?

得分: 0 / 5