前言
从多态性和代码重用的角度来看,一般推荐的做法是:将类型的共享行为和类型公共属性进行分离,相应代码区块保留针对自身的唯一方法。在这样做的过程中,要确保不同的类型通过这些公共属性相互关联,这就使得可以为参数编写更通用或更具包容性的API功能代码。如此的目的是:着我们可以使用具有这些共享属性的类型,同时又不受特定类型的限制。
在Java或C#等面向对象(object-oriented)的语言中,程序接口(interfaces)传达了同样的思想,于其中可以定义许多类型都可以实现的共享行为。例如,与其使用多个接受整数值列表的sort函数和其他接受字符串值列表的函数,不如使用一个可以接受实现Comparable或Comparator接口的项列表的sort函数,而这中设置允许我们传递任何Comparable内容给sort函数。
Rust语言也有一个类似且强大的结构设计,称为特性(Traits)。Rust中有很多形式的特性,这里将简要的过一下其中的大部分内容,以及与其互动的方式。此外,当特性与泛型混合使用时,我们可以传递给API的参数限定范围。当后续学到更多关于特性边界(trait bounds)内容时,会有这方面的实例。
特性(Traits)
特性是定义了一组具有契约或共享行为性质的项,可以为类型所选择并实现。由此可见,特性本身是不可用的,事实上要由类型来实现。特性有能力在不同类型之间建立关系,是许多语言特性的支柱,包括闭包、操作符、智能指针、循环、编译时数据竞争检查,等等。在Rust中,相当多的高级语言特性归结为某种类型,并为其调用实现的trait方法。说到这里,让我们看看如何定义和使用Rust中特性。
比如我们正在为一个可以播放音频和视频文件的简单媒体播放器应用程序建模。在这个demo中,首先运行cargo new super_player创建一个项目。为了传达特性的概念,方便起见,将音频和视频媒体表示为元组结构体,名称为String,如下所示:
// super_player/src/main.rs
struct
Audio
(
String
);
struct
Video
(
String
);
fn
main
()
{
// stuff
}
不难想到,从最精简的角度,Audio和Video这两个结构都需要有个play和pause的功能方法,因此没必要分别写了,正好可以用trait。在这里,我们将在一个名为media.rs的单独模块中定义一个名为Playable的特性,它有两个方法,如下所示:
// super_player/src/media.rs
trait Playable
{
fn
play
(
&
self
);
fn
pause
()
{
println
!
(
"Paused"
);
}
}
我们使用trait关键字创建一个特性Playable,后面跟着它的名称和一对大括号。在大括号内,可以提供零个或多个方法,任何实现该特性的类型都应该满足这些方法;还可以在特性中定义常量,所有实现者都可以共享这些常量。实现的主体或行为者可以是任何结构体(struct) 、枚举(enum)、原语(primitive)、函数 (function)、闭包(closure),甚至是特征(trait)。
读者朋友可能已经注意到了play函数,它接受对符号self的引用,但没有内容,就以分号结束了。实际上,self只是Self的类型别名,指的是trait被实现的类型(请记住这一点,我会在第7章“高级概念”中详细讨论这些)。这意味着特征中的方法就像来自Java的抽象方法,取决于实现这个特性的类型,并根据具体用例定义函数。当然,trait内声明的方法也可以有默认实现,比如上面代码中的pause函数,而pause不接收self,因此类似于不需要实现行为调用实例的静态方法。
这里小结一下特性内部的两个方法:
- Associated methods:这种方法可以直接在实现特性的类型上使用,不需要该类型的实例进行调用。这种方式也就是主流语言中所谓的静态方法,例如,标准库中FromStr特性中的from_str方法。它是为字符串实现的,因此可以通过调用String::from_str("foo")以&str来创建字符串
- Instance methods:该类方法的第一个参数为self,仅在实现trait的类型的实例上可用。
- self指向实现trait的类型的实例,有三种类型:
- self方法,在被调用时使用实例;
- &self方法,只对实例的成员读取
- &mut self方法,它们可以可变地访问其成员,并可以修改它们,甚至用另一个实例替换它们。
略谈一点,标准库中的AsRef trait中的as_ref方法是一个接收&self的实例方法,是由可以转换为引用或指针的类型来实现的。在第5章内存管理和安全的内容中,会再讨论下这些方法在引用,类型签名方面的&和&mut&的内容。
现在,在Audio和Video类型上实现前面的Playable特性,代码如下:
// super_player/src/main.rs
struct
Audio
(
String
);
struct
Video
(
String
);
impl Playable
for
Audio
{
fn
play
(
&
self
)
{
println
!
(
"Now playing: {}"
,
self
.
0
);
}
}
impl Playable
for
Video
{
fn
play
(
&
self
)
{
println
!
(
"Now playing: {}"
,
self
.
0
);
}
}
fn
main
()
{
println
!
(
"Super player!"
);
}
上述代码有关实现的用法,之前都已经介绍过,但为trait写实现,略有不同。先使用impl关键字,然后是trait名称,然后是for关键字和需要实现trait的类型。在这些大括号内,需要提供方法的实现,同时可以选择覆盖特性中存在的任何默认设定。编译一下,看下结果:
上述错误体现了trait的一个重要特性:trait默认是私有的。为了让其他模块或crate能够使用,需要设置成公开。一般两个步骤。首先,需要向外界声明特性,也就是加上pub关键字:
// super_player/src/media.rs
pub trait Playable
{
fn
play
(
&
self
);
fn
pause
()
{
println
!
(
"Paused"
);
}
}
再就是按照上图结果的提示,加上use关键字,那么全部主函数的代码如下:
// super_player/src/main.rs
mod media
;
use media
::
Playable
;
struct Audio
(
String
);
struct Video
(
String
);
impl Playable
for
Audio
{
fn play
(
&
self
)
{
println
!
(
"🎵 Now playing: {}"
,
self
.
0
);
}
}
impl Playable
for
Video
{
fn play
(
&
self
)
{
println
!
(
"🎵 Now playing: {}"
,
self
.
0
);
}
}
fn main
()
{
println
!
(
"Super player!"
);
let
audio
=
Audio
(
"ambient_music.mp3"
.
to_string
());
let
video
=
Video
(
"big_buck_bunny.mkv"
.
to_string
());
audio
.
play
();
video
.
play
();
}
编译通过,结果如下
当然上述的结果太假了,但作为特性的演示,还是够用的。
结语
对于任何开发者而言,面对如此灵活的类型,心里多少还是有所宽慰的,毕竟能够省去很多重复搬砖的内容。
主要参考和建议读者进一步阅读的文献
https://doc.rust-lang.org/book
Rust编程之道,2019, 张汉东
The Complete Rust Programming Reference Guide,2019, Rahul Sharma,Vesa Kaihlavirta,Claus Matzinger
Hands-On Data Structures and Algorithms with Rust,2018,Claus Matzinger
Beginning Rust ,2018,Carlo Milanesi
Rust Cookbook,2017,Vigneshwer Dhinakaran