写给前端看的Rust教程(10)从 Mixins 到 Traits

2,999 阅读6分钟

原文:24 days from node.js to Rust

前言

为了实现代码复用,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 ,不过会更加复杂一些

RustTraitsJavaScript中的mixins非常相似,它们是一堆方法的集合,市面上也有不少文档将structTraits与对象继承相比较,不要理会那些,它们只会把问题搞复杂

你只需要记住,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) {...}

在这个示例里,我们不关心具体传入的数据到底是什么类型,我们希望的只是拿到它的namestate,这时候Traits就派上用场了

Traits

Traits的定义以trait关键字作为开头,结构和impl类似,包含了若干方法,唯一不同之处在于Traits中的方法可以没有函数体

注意:函数体是可选的,如果写了,就相当于是默认的实现,其他人可以选择override默认的实现

现在实现一个名为LightTraits,并添加一个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的模块系统是我所见过的最简单的,所以在最开始的学习过程中你可能会体会到一些困难,不过在克服掉少数几个困难后,你会很快掌握这套系统

更多