以下内容为本人学习过程中的记录笔记,其中可能存在不准确或错误,欢迎勘误及指正

Trait的作用

在Rust中Trait(特质)是一种任何类型都可以选择支持或不支持的一种定义行为的机制,Trait可以被认为是某类型能够做什么的一种能力,其类似于其他语言中的接口,但存在一定区别。Trait的作用主要包含(1)定义共享行为、(2)实现多态、(3)扩展类型的功能、(4)提高代码可读性和可维护性、(5)约束泛型类型(见泛型部分)

Trait的定义与实现

Trait的定义

使用trait关键字定义一个Trait。Trait的定义是将方法的签名进行封装,以定义实现某种目的所必须的一组行为

1
2
3
4
5
6
trait Shape {
// 定义计算面积的方法签名
fn area(&self) -> f64;
// 定义计算周长的方法签名
fn perimeter(&self) -> f64;
}

在Trait的定义中只有方法签名而没有具体的实现,其中封装的方法的具体实现由实现该Trait的类型提供

Trait的实现

Trait中定义方法的实现和结构体或枚举方法的实现很相似,都是使用impl关键字。具体写法为impl ... for ...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
trait Shape {
fn area(&self) -> f64;
fn perimeter(&self) -> f64;
}

struct Rectangle {
width: f64,
height: f64,
}

// 为Rectangle提供Shape trait实现
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
}

struct Circle {
radius: f64,
}

// 为Circle提供Shape trait实现
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn perimeter(&self) -> f64 {
2.0 * std::f64::consts::PI * self.radius
}
}

fn main() {
let rec = Rectangle {
width: 12.0,
height: 20.0,
};

// 调用已经实现Trait中的方法也和调用普通方法一致
println!("area: {}, perimeter: {}", rec.area(), rec.perimeter());
}

在上面的例子中,“圆”和“矩形”是不同的类型,但我们可以在函数中使用同样的函数签名来调用它们绑定的方法

Trait的实现约束

在类型上实现Trait存在两个限制条件

  1. 这个类型`或`这个Trait(至少有一个)是在本地crate定义的 ,比如我们可以为Rectangle类型实现Debug Trait,也可以为Vector类型实现Shape Trait
  2. 为确保两个不同库的代码不互相影响,在Rust中无法为外部类型实现外部Trait ,比如我们无法为标准库中的String类型实现Display Trait

Trait默认实现与重载

Trait的默认实现

在前面,我们说Trait所封装的方法的实现由实现这个Trait的类型提供,但在定义Trait时其实也可以提供默认实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
trait Shape {
fn perimeter(&self) -> f64;
// 提供默认实现
fn area(&self) -> f64 {
println!("default method, return perimeter");
// 在默认实现中可以调用trait中定义的方法,且不用管其他方法是否提供了默认实现
self.perimeter()
}
}

struct Rectangle {
width: f64,
height: f64,
}

impl Shape for Rectangle {
// 当一个类型想调用默认实现时,不需要为该类型实现已经默认实现的方法
fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
}

fn main() {
let rec = Rectangle {
width: 12.0,
height: 20.0,
};
// 这里的代码调用了Shaper trait中的默认实现,最后打印的是周长
println!("rec area: {}", rec.area());
}

上面的例子中,Trait在定义时就提供了默认实现。但需要注意的是,默认实现无法获得类型中的字段(field) ,因为Trait定义的是具体类型的共享行为,它无法知道用户定义的类型会提供什么数据,比如我们在矩形中我们提供了长和宽,而圆只提供了半径

Trait的重载

虽然Trait可以提供默认实现,但我们也可以针对特定类型进行Trait的重载,重载不会影响其他类型调用默认实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
trait Shape {
fn perimeter(&self) -> f64;
fn area(&self) -> f64 {
println!("default method, return perimeter");
self.perimeter()
}
}

struct Circle {
radius: f64,
}

impl Shape for Circle {
fn perimeter(&self) -> f64 {
2.0 * std::f64::consts::PI * self.radius
}
// 重载默认实现
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}

fn main() {
let cir = Circle {
radius: 5.0,
};
// 因为Circle重载了area()方法,所以打印的是正确的面积值
println!("cir area: {}", cir.area());
}

Trait作为传入类型和返回类型

作为传入类型

在Rust中,我们可以将Trait作为函数传入的参数类型,就可以实现将多种具体的类型传入同一函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
trait Shape {
fn perimeter(&self) -> f64;
fn area(&self) -> f64;
}

struct Rectangle {
width: f64,
height: f64,
}

impl Shape for Rectangle {
fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
fn area(&self) -> f64 {
self.width * self.height
}
}

fn main() {
let rec = Rectangle {
width: 12.0,
height: 20.0,
};

trait_param(rec);
}

// 这个函数接收一个实现Shape trait的类型
fn trait_param(item: impl Shape) {
println!("{}", item.area());
}

在上面的例子中,trait_param()这个函数接收一个实现了Shape trait的类型,在调用时可以将任何实现Shape trait的类型传入。当我们想约束传入的类型需要实现多个Trait时,Trait之间可以使用+号连接

作为返回类型

和传入参数时类似,Trait也可以作为返回类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
use std::fmt::Debug;

trait Shape {
fn perimeter(&self) -> f64;
fn area(&self) -> f64;
}

#[derive(Debug)]
struct Rectangle {
width: f64,
height: f64,
}

impl Shape for Rectangle {
fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
fn area(&self) -> f64 {
self.width * self.height
}
}

fn main() {
let rec = return_trait();
println!("{:?}", rec);
}

// 这个函数会返回一个实现了Shape trait和Debug trait的类型
fn return_trait() -> impl Shape + Debug {
Rectangle {
width: 11.0,
height: 9.0,
}
}

上面的例子中,return_trait()函数会返回一个实现Shape trait和Debug trait的类型。但是需要注意的是,当我们使用特定trait作为返回类型时,用户必须保证这个函数返回的具体类型是确定的 ,否则就会像下面例子中的代码一样出现错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
use std::fmt::Debug;

trait Shape {
fn perimeter(&self) -> f64;
fn area(&self) -> f64;
}

#[derive(Debug)]
struct Rectangle {
width: f64,
height: f64,
}

impl Shape for Rectangle {
fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
fn area(&self) -> f64 {
self.width * self.height
}
}

#[derive(Debug)]
struct Circle {
radius: f64,
}

impl Shape for Circle {
fn perimeter(&self) -> f64 {
2.0 * std::f64::consts::PI * self.radius
}
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}

fn main() {
let rec = return_trait(false);
println!("{:?}", rec);
}

// 以下的函数会出现一个错误,因为函数返回的具体类型的可能性不唯一
fn return_trait(flag: bool) -> impl Shape + Debug {
if flag {
Rectangle {
width: 11.0,
height: 9.0,
}
} else {
Circle { radius: 7.0 }
}
}

以上的例子是无法通过编译的,但如果我们确实需要实现与例子中return_trait()函数类似的功能也是有办法的,具体见Trait对象部分