[ Rust笔记 五] 基础-trait

成员方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
trait Shape {
fn area(&self) -> f64;//关联函数,参数被称为接收者
}

trait T {
fn method1(self: Self);
fn method2(self: &Self);
fn method3(self: &mut Self);
}
// 上下两种写法是完全一样的
trait T {
fn method1(self);
fn method2(&self);
fn method3(&mut self);
}

具有receiver参数的函数,我们称为“方法”​(method)​,可以通过变量实例使用小数点来调用。
没有receiver参数的函数,我们称为“静态函数”​(static function)​,可以通过类型加双冒号::的方式来调用。

Rust中Self(大写S)和self(小写s)都是关键字,大写S的是类型名,小写s的是变量名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Circle {
radius: f64,
}
impl Shape for Circle {
// Self 类型就是 Circle
// self 的类型是 &Self,即 &Circle
fn area(&self) -> f64 {
// 访问成员变量,需要用 self.radius
std::f64::consts::PI * self.radius * self.radius
}
}
fn main() {
let c = Circle { radius : 2f64};
// 第一个参数名字是 self,可以使用小数点语法调用
println!("The area is {}", c.area());
}

另一种写法“内在方法”

1
2
3
impl Circle {
fn get_radius(&self) -> f64 { self.radius }
}

trait中可以包含方法的默认实现。如果这个方法在trait中已经有了方法体,那么在针对具体类型实现的时候,就可以选择不用重写。

self参数甚至可以是Box指针类型self

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
trait Shape {
fn area(self: Box<Self>) -> f64;
}
struct Circle {
radius: f64,
}
impl Shape for Circle {
// Self 类型就是 Circle
// self 的类型是 Box<Self>,即 Box<Circle>
fn area(self : Box<Self>) -> f64 {
// 访问成员变量,需要用 self.radius
std::f64::consts::PI * self.radius * self.radius
}
}
fn main() {
let c = Circle { radius : 2f64};
// 编译错误
// c.area();
let b = Box::new(Circle {radius : 4f64});
// 编译正确
b.area();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
trait Shape {
fn area(self: Box<Self>) -> f64;
}
struct Circle {
radius: f64,
}
impl Shape for Circle {
// Self 类型就是 Circle
// self 的类型是 Box<Self>,即 Box<Circle>
fn area(self : Box<Self>) -> f64 {
// 访问成员变量,需要用 self.radius
std::f64::consts::PI * self.radius * self.radius
}
}
fn main() {
let c = Circle { radius : 2f64};
// 编译错误
// c.area();
let b = Box::new(Circle {radius : 4f64});
// 编译正确
b.area();
}

impl的对象甚至可以是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
trait Shape {
fn area(&self) -> f64;
}

trait Round {
fn get_radius(&self) -> f64;
}
struct Circle {
radius: f64,
}
impl Round for Circle {
fn get_radius(&self) -> f64 { self.radius }
}
// 注意这里是 impl Trait for Trait
impl Shape for Round {
fn area(&self) -> f64 {
std::f64::consts::PI * self.get_radius() * self.get_radius()
}
}
fn main() {
let c = Circle { radius : 2f64};
// 编译错误
// c.area();
let b = Box::new(Circle {radius : 4f64}) as Box<Round>;
// 编译正确
b.area();
}

注意这里的写法,impl Shape for Round和impl<T: Round> Shape for T是不一样的。在前一种写法中,self是&Round类型,它是一个trait object,是胖指针。而在后一种写法中,self是&T类型,是具体类型。前一种写法是为trait object增加一个成员方法,而后一种写法是为所有的满足T: Round的具体类型增加一个成员方法。

在声明trait和impl trait的时候,Rust规定了一个Coherence Rule(一致性规则)或称为Orphan Rule(孤儿规则):impl块要么与trait的声明在同一个的crate中,要么与类型的声明在同一个crate中。

静态方法

没有receiver参数的方法(第一个参数不是self参数的方法)。通过Type::FunctionName()的方式调用。

1
2
3
4
5
6
7
8
9
10
11
12
struct T(i32);
impl T {
// 这是一个静态方法
fn func(this: &Self) {
println!{"value {}", this.0};
}
}
fn main() {
let x = T(42);
// x.func(); 小数点方式调用是不合法的
T::func(&x);
}

trait中也可以定义静态函数

1
2
3
4
5
6
7
8
9
10
pub trait Default {
fn default() -> Self;
}

// 这里用到了“泛型”,请参阅第21章
impl<T> Default for Vec<T> {
fn default() -> Vec<T> {
Vec::new()
}
}

这样的写法是错误的。请一定要记住,trait的大小在编译阶段是不固定的。

1
2
3
let x: Shape = Circle::new(); // Shape 不能做局部变量的类型
fn use_shape(arg : Shape) {} // Shape 不能直接做参数的类型
fn ret_shape() -> Shape {} // Shape 不能直接做返回值的类型

目前版本的Rust规定,在函数参数传递、返回值传递等地方,都要求这个类型在编译阶段有确定的大小。

完整函数调用语法

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
trait Cook {
fn start(&self);
}
trait Wash {
fn start(&self);
}
struct Chef;
impl Cook for Chef {
fn start(&self) { println!("Cook::start"); }
}
impl Wash for Chef {
fn start(&self) { println!("Wash::start"); }
}

fn main() {
let me = Chef;
me.start();// 出现歧义

}

fn main() {//无歧义
let me = Chef;
// 函数名字使用更完整的path来指定,同时,self参数需要显式传递
<Cook>::start(&me);
<Chef as Wash>::start(&me);
}

trait约束和继承

1
2
3
4
5
6
7
8
9
10
use std::fmt::Debug;
fn my_print<T : Debug>(x: T) {
println!("The value is {:? }.", x);
}
fn main() {
my_print("China");
my_print(41_i32);
my_print(true);
my_print(['a', 'b', 'c'])
}

冒号后面加trait名字,就是这个泛型参数的约束条件。它要求这个T类型实现Debug这个trait。这是因为我们在函数体内,用到了println!格式化打印,而且用了{:? }这样的格式控制符,它要求类型满足Debug的约束,否则编译不过。

另外写法

1
2
3
fn my_print<T>(x: T) where T: Debug {
println!("The value is {:? }.", x);
}

trait继承

1
2
3
4
5
6
7
trait Base {}
trait Derived : Base {}
struct T;
impl Derived for T {}
impl Base for T {} //必须实现Base和Derived,必须两个都实现,不然编译报错
fn main() {
}

Derive

1
2
3
4
5
6
7
8
9
#[derive(Copy, Clone, Default, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]//derive称为attribute,等于实现了列表中所有的trait
struct Foo {
data : i32
}
fn main() {
let v1 = Foo { data : 0 };
let v2 = v1;
println!("{:? }", v2);
}

支持的derive有如下这些

1
2
Debug    Clone    Copy    Hash    RustcEncodable    RustcDecodable    PartialEq    Eq
ParialOrd Ord Default FromPrimitive Send Sync

标准库中常见的trait简介

Display和Debug

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
// std::fmt::Display
pub trait Display {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error>;
}
// std::fmt::Debug
pub trait Debug {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error>;
}

use std::fmt::{Display, Formatter, Error};
#[derive(Debug)]
struct T {
field1: i32,
field2: i32,
}
impl Display for T {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
write! (f, "{{ field1:{}, field2:{} }}", self.field1, self.field2)
}
}
fn main() {
let var = T { field1: 1, field2: 2 };
println!("{}", var); //实现了Display trait
println!("{:? }", var);//实现了Debug trait,紧凑模式,打印struct
println!("{:#? }", var);//实现了Debug trait,美化模式,打印struct有换行
}

❏ Display假定了这个类型可以用utf-8格式的字符串表示,它是准备给最终用户看的,并不是所有类型都应该或者能够实现这个trait。这个trait的fmt应该如何格式化字符串,完全取决于程序员自己,编译器不提供自动derive的功能。

❏ 标准库中还有一个常用trait叫作std::string::ToString,对于所有实现了Display trait的类型,都自动实现了这个ToString trait。它包含了一个方法to_string(&self) -> String。任何一个实现了Display trait的类型,我们都可以对它调用to_string()方法格式化出一个字符串。

❏ Debug则是主要为了调试使用,建议所有的作为API的“公开”类型都应该实现这个trait,以方便调试。它打印出来的字符串不是以“美观易读”为标准,编译器提供了自动derive的功能。

PartialOrd / Ord / PartialEq / Eq

❏ 如果a < b则一定有! (a > b);反之,若a > b,则一定有!(a < b),称为反对称性。

❏ 如果a < b且b < c则a < c,称为传递性。

❏ 对于X中的所有元素,都存在a < b或a > b或者a == b,三者必居其一,称为完全性。

满足其中两个称为偏序,全满足为全序。

**浮点数为偏序,因为NaN的存在。**导致浮点数无法排序

Rust设计了两个trait来描述偏序和全序。

1
2
3
4
5
6
7
8
9
10
11
pub trait PartialOrd<Rhs: ? Sized = Self>: PartialEq<Rhs> {
fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>; //可能返回空或者Ordering
fn lt(&self, other: &Rhs) -> bool { //... }
fn le(&self, other: &Rhs) -> bool { //... }
fn gt(&self, other: &Rhs) -> bool { //... }
fn ge(&self, other: &Rhs) -> bool { //... }
}

pub trait Ord: Eq + PartialOrd<Self> {
fn cmp(&self, other: &Self) -> Ordering;//返回Ordering
}

f32和f64类型都只实现了PartialOrd,而没有实现Ord。

Sized

这个trait定义在std::marker模块中,它没有任何的成员方法。它有#[lang =”sized”]属性,说明它与普通trait不同,编译器对它有特殊的处理。用户也不能针对自己的类型impl这个trait。一个类型是否满足Sized约束是完全由编译器推导的,用户无权指定。

在Rust中,但凡编译阶段能确定大小的类型,都满足Sized约束。

Rust中对于动态大小类型专门有一个名词Dynamic Sized Type。我们后面将会看到的[T], str以及dyn Trait都是DST。

Default

对于那种无参数、无错误处理的简单情况,标准库中提供了Default trait来做这个统一抽象。

1
2
3
trait Default {
fn default() -> Self;
}

总结

本章对trait这个概念做了基本的介绍。除了上面介绍的之外,trait还有许多用处:
❏ trait本身可以携带泛型参数;

❏ trait可以用在泛型参数的约束中;

❏ trait可以为一组类型impl,也可以单独为某一个具体类型impl,而且它们可以同时存在;

❏ trait可以为某个trait impl,而不是为某个具体类型impl;

❏ trait可以包含关联类型,而且还可以包含类型构造器,实现高阶类型的某些功能;

❏ trait可以实现泛型代码的静态分派,也可以通过trait object实现动态分派;

❏ trait可以不包含任何方法,用于给类型做标签(marker)​,以此来描述类型的一些重要特性;

❏ trait可以包含常量。trait这个概念在Rust语言中扮演了非常重要的角色,承担了各种各样的功能,在写代码的时候会经常用到。

本章还远没有把trait相关的知识讲解完整,更多关于trait的内容,请参阅本书后文中与泛型、trait object线程安全有关的章节。