前言
为了实现代码复用,Rust
设计了Traits
。这和JavaScript
中的mixins
非常相似,那是一种向object
中添加方法的模式,通常会用到Object.assign()
,例如:
const utilityMixin = {
prettyPrint() {
console.log(JSON.stringify(this, null, 2));
}
};
class Person {
constructor(first, last) {
this.firstName = first;
this.lastName = last;
}
}
function mixin(base, mixer) {
Object.assign(base.prototype, mixer);
}
mixin(Person, utilityMixin);
const author = new Person("Jarrod", "Overson");
author.prettyPrint();
你也可以在TypeScript
中使用 mixins ,不过会更加复杂一些
Rust
的Traits
和JavaScript
中的mixins
非常相似,它们是一堆方法的集合,市面上也有不少文档将struct
和Traits
与对象继承相比较,不要理会那些,它们只会把问题搞复杂
你只需要记住,Traits
就是一堆方法的集合
正文
完善TrafficLight
在前面的文章中,我们给TrafficLight
结构体增加了get_state()
方法,现在是时候给每种灯光添加功能了,我们要添加的第一个灯光是家庭灯光。不需要考虑太多功能,只要能开和关即可
不出意外,我们可以如下实现:
#[derive(Debug)]
struct HouseLight {
on: bool,
}
impl Display for HouseLight {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Houselight is {}", if self.on { "on" } else { "off" })
}
}
impl HouseLight {
pub fn new() -> Self {
Self { on: false }
}
pub fn get_state(&self) -> bool {
self.on
}
}
接下来,我们实现一个通用的print_state()
函数,我们希望这个函数可以打印所有灯光的状态:
fn print_state(light: ???) {
}
我们该怎么做呢?我们无法像在TypeScript
那样列出所有类型的列表:
function print(light: TrafficLight | HouseLight) {...}
在这个示例里,我们不关心具体传入的数据到底是什么类型,我们希望的只是拿到它的name
和state
,这时候Traits
就派上用场了
Traits
Traits
的定义以trait
关键字作为开头,结构和impl
类似,包含了若干方法,唯一不同之处在于Traits
中的方法可以没有函数体
注意:函数体是可选的,如果写了,就相当于是默认的实现,其他人可以选择
override
默认的实现
现在实现一个名为Light
的Traits
,并添加一个get_name()
方法:
trait Light {
fn get_name(&self) -> &str;
}
实现Traits
的时候,我们采用impl
关键字,就像我们在struct
上做的一样,现在我们按照impl [trait] for [struct]
的格式来写:
impl Light for HouseLight {
fn get_name(&self) -> &str {
"House light"
}
}
impl Light for TrafficLight {
fn get_name(&self) -> &str {
"Traffic light"
}
}
现在我们就可以实现print_state()
函数了,接收的参数写impl [trait]
:
fn print_state(light: &impl Light) {
println!("{}", light.get_name());
}
如果你想在trait
中添加get_state()
方法,我们会遇到一个困难,因为每种灯光的state
有不同的类型,所以我们用debug
格式来打印它们,听到这,你的的第一反应可能是按下面这么写:
trait Light {
fn get_name(&self) -> &str;
fn get_state(&self) -> impl std::fmt::Debug;
}
但这是行不通的,rust
会报出impl Trait not allowed outside of function and method return types
:
error[E0562]: `impl Trait` not allowed outside of function and method return types
--> crates/day-10/traits/src/main.rs:17:27
|
17 | fn get_state(&self) -> impl std::fmt::Debug;
| ^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0562`.
但我们就是要将其当做返回值,这要怎么做到呢?
impl vs dyn
在这里如果想使用Traits
需要利用dyn [trait]
,究竟使用dyn [trait]
还是使用impl [trait]
,关键之处在于Rust
能否在编译期间知晓具体的类型。我们这里无法使用impl std::fmt::Debug
的原因在于每种具体的实现都会返回一个不同的类型,而使用dyn
则是牺牲了性能换取灵活性。一旦一个数值以dyn
来描述,则它会丢失类型信息,本质上就是一个指向trait
方法的二进制数据
所以我们将代码进行如下改造:
trait Light {
fn get_name(&self) -> &str;
fn get_state(&self) -> &dyn std::fmt::Debug;
}
impl Light for HouseLight {
fn get_name(&self) -> &str {
"House light"
}
fn get_state(&self) -> &dyn std::fmt::Debug {
&self.on
}
}
impl Light for TrafficLight {
fn get_name(&self) -> &str {
"Traffic light"
}
fn get_state(&self) -> &dyn std::fmt::Debug {
&self.color
}
}
注意:
Rust
必须在编译期间知道每一个变量的size
,但是对于dyn [trait]
是个例外,因为它不是任何具体的类型,不具备已知的size
现在我们的完整代码如下:
use std::fmt::Display;
fn main() {
let traffic_light = TrafficLight::new();
let house_light = HouseLight::new();
print_state(&traffic_light);
print_state(&house_light);
}
fn print_state(light: &impl Light) {
println!("{}'s state is : {:?}", light.get_name(), light.get_state());
}
trait Light {
fn get_name(&self) -> &str;
fn get_state(&self) -> &dyn std::fmt::Debug;
}
impl Light for HouseLight {
fn get_name(&self) -> &str {
"House light"
}
fn get_state(&self) -> &dyn std::fmt::Debug {
&self.on
}
}
impl Light for TrafficLight {
fn get_name(&self) -> &str {
"Traffic light"
}
fn get_state(&self) -> &dyn std::fmt::Debug {
&self.color
}
}
impl std::fmt::Display for TrafficLight {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Traffic light is {}", self.color)
}
}
#[derive(Debug)]
struct TrafficLight {
color: TrafficLightColor,
}
impl TrafficLight {
pub fn new() -> Self {
Self {
color: TrafficLightColor::Red,
}
}
pub fn turn_green(&mut self) {
self.color = TrafficLightColor::Green
}
}
#[derive(Debug)]
enum TrafficLightColor {
Red,
Yellow,
Green,
}
impl Display for TrafficLightColor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let color_string = match self {
TrafficLightColor::Green => "green",
TrafficLightColor::Red => "red",
TrafficLightColor::Yellow => "yellow",
};
write!(f, "{}", color_string)
}
}
#[derive(Debug)]
struct HouseLight {
on: bool,
}
impl Display for HouseLight {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Houselight is {}", if self.on { "on" } else { "off" })
}
}
impl HouseLight {
pub fn new() -> Self {
Self { on: false }
}
}
打印结果是:
[snipped]
Traffic light's state is : Red
House light's state is : false
现在我们的代码体积变的越来越大,是时候改学习一下如何拆分代码了
延伸阅读
总结
在Rust
世界里Traits
到处都是,非常值得深入研究一下,你可以在Github
上阅读Rust
的代码,也可以全看看标准库的实现。对于某些语言来说,可能存在唯一正确的方法去实现某个功能,但在Rust
中不存在类似的情况,在Rust
中我们有1000种不同的途径去实现某个功能。多去阅读别人的代码,不要闭门造车,这点在学习Rust
的时候非常重要
注意:有1000种方法去实现某个功能这件事,让
Rust
成了Perl
的精神继承者
在下一篇文章中,我们会介绍模块系统。你可以很快学会,不过鉴于你可能是从Node.js
转过来的,而Node.js
的模块系统是我所见过的最简单的,所以在最开始的学习过程中你可能会体会到一些困难,不过在克服掉少数几个困难后,你会很快掌握这套系统