前言
这里紧接着上篇的内容来说。
特性用法
特性也可以在其声明中说明依赖于其他特性,这就是所谓特性继承(trait inheritance),用法如下
// trait_inheritance.rs
trait Vehicle {
fn
get_price
(
&
self
)
->
u64
;
}
trait Car
:
Vehicle
{
fn
model
(
&
self
)
->
String
;
}
struct TeslaRoadster
{
model
:
String
,
release_date
:
u16
}
impl TeslaRoadster
{
fn
new
(
model
:
&
str
,
release_date
:
u16
)
->
Self
{
Self
{
model
:
model
.
to_string
(),
release_date
}
}
}
impl Car
for
TeslaRoadster
{
fn
model
(
&
self
)
->
String
{
"Tesla Roadster I"
.
to_string
()
}
}
fn
main
()
{
let
my_roadster
=
TeslaRoadster
::
new
(
"Tesla Roadster II"
,
2020
);
println
!
(
"{} is priced at ${}"
,
my_roadster
.
model
,
my_roadster
.
get_price
());
}
在上面的代码中,我们声明了两个特性:一个是Vehicle(相对广泛的特性),另一个一个是Car(相对具体的特性),显然依赖于Vehicle。由于TeslaRoadster是一辆汽车,可以为其实现Car特性。另外,请注意TeslaRoadster上的new方法,其使用Self作为返回类型,可被替换为从new返回的TeslaRoadster实例。Self只是特性在impl块中实现类型的一个简单别名,还可以用于创建其他类型,比如元组结构和枚举,也可以用于匹配表达式。我们编译一下这段代码:
说明一点,后续文字会大量使用trait来替代特性这个词。
此时,看到上图这个错误了吧?在其定义中,Car trait指定了一个依赖关系,即任何实现该trait的类型也必须实现Vehicle trait Car: Vehicle。这里没有为TeslaRoadster实现Vehicle,Rust编译器以此报错。所以,必须添加相关的实现代码,如下
// trait_inheritance.rs
impl Vehicle for
TeslaRoadster
{
fn
get_price
(
&
self
)
->
u64
{
200
_000
//underscore here is just for readability
}
}
编译通过,结果如下
与面向对象的语言类似,trait及其实现类似于接口(interface)和实现这些接口的类(class)。然而,需要注意的是,trait与interface在Rust中还是有明显区别的:
- trait在Rust中具有某种形式的继承,实现(implementation)却没有。这意味着可以声明一个名为Panda的trait,需要另一个名为KungFu的trait,其有实现Panda的类型来实现(有点像绕口令,但意思上就是这样)。然而,类型本身没有任何类型的继承。因此,不是使用对象继承,而是使用类型组合,其依赖于trait继承来在代码中为现实实体建模。
- 可以在任何地方编写trait实现块,而不必访问实际的类型
- 可以在从内置基元类型到泛型类型的任何类型上实现trait
- 在函数中不能隐式的将特征作为返回类型,这与在Java中可以将接口作为返回类型是不一样的。必须返回一个trait对象,相关语法是很明确的,我们会在讲到trait对象时介绍。
Trait的形式
在前面的例子中,我们已经见识了trait的最简单形式,但其作用远比表面上看到的要大。当遇到大型的代码库时,会发现有很多各种各样的特性。考虑到问题和相关程序的复杂性,简单的trait可能就不太适合了。Rust为我们提供了更多形式的特性,可以很好的模拟复杂问题。下面来看看一些标准库的trait,并试着对其进行分类,这样就能渐渐了解到什么时候使用什么。
Marker traits
在std::marker模块中定义的性状称为marker traits。这些trait没有任何方法,只是用它们的名称和一个空体声明。标准库中的示例包括Copy、Send和Sync。之所以被称为marker traits,因为它们被用来简单的标记一个类型属于一个特定的类别,以获得一些编译时的保障。标准库中的两个例子是Send和Sync,对大多数类型而言,这些trait在特定的时候自动实现,并传达关于哪些值可以安全的在线程间发送和共享的信息。有关进一步介绍,我们将在第8章 讲并发性时继续。
Simple traits
这就是上一小节介绍的简单的traits
trait Foo
{
fn
foo
();
}
标准库中的一个例子是Default trait,它是为可以用默认值初始化的类型来进行实现的。在https://doc.rust-lang.org/std/default/trait.Default.html上有相关的文档。
Generic traits
Trait也可以是泛型类别的,这在为各种各样的类型实现一个trait的场景中很有用:
pub trait From
<
T
>
{
fn
from
(
T
)
->
Self
;
}
举两个这样的例子:From<T>和Into<T>,这两个可以用作从类型到类型T的转换,反之亦然。当这些trait被用作函数参数的trait边界时,相应的效果就变得尤为突出。稍后我们会看到trait边界是什么,以及它们是如何起作用的。也有个问题,就是当用三到四种泛型类型声明generic trait时,可能会变得非常冗长。对于这些情况,就引出下面要讲的associated type traits。
Associated type traits
trait Foo
{
type Out
;
fn
get_value
(
self
)
->
Self
::
Out
;
}
由于能够在trait中声明相关类型,所以associated type traits是generic traits的不错替代,比如上述代码中Foo所声明Out类型,相应类型签名不那么冗长。其优点在于,在实现过程中,允许开发者声明关联的类型一次,并在任何trait方法或函数中使用Self::Out作为返回类型或参数类型。像泛型特征一样,这就消除了有关类型的不必要说明。Associated type traits的一个最好的例子是Iterator trait,它用于迭代自定义类型的值,其文档可以在https://doc.rust-lang.org/std/iter/trait.Iterator.html上找到。到了第8章的高级概念中,我们将更深入的研究Iterator。
Inherited traits
我们已经在trait_inheritance.rs中看到了这种trait的例子。与Rust中的类型不同,traits可以有继承关系,例如:
trait Bar
{
fn
bar
();
}
trait Foo
:
Bar
{
fn
foo
();
}
在上面的代码片段中,我们声明了一个trait Foo,它依赖于一个超级trait Bar。Foo的定义要求在为类型实现Foo时,也要实现Bar。标准库中的一个例子是Copy trait,它要求类型也实现Clone trait。
结语
下一篇开始,我们介绍特性绑定(trait bounds)。
6
+
主要参考和建议读者进一步阅读的文献
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