写给前端看的Rust教程(9)语言篇[下]

1,290 阅读7分钟

原文:24 days from node.js to Rust

前言

在上一篇文章 《写给前端看的Rust教程(8)语言篇[中]》我们介绍了如何创建struct,我们已经知道了rust中的structJavaScript中的class有相似之处,本文我们将继续学习struct的方法以及match表达式

注意:我们使用TypeScript会开始多于JavaScript,你需要安装ts-nodenpm install -g ts-node)来运行文中的TypeScript示例

struct添加方法

我们昨天增加的new方法类似TypeScript中的static函数,你可以通过名字直接调用而不必实例化:

impl TrafficLight {
  pub fn new() -> Self {
    Self {
      color: "red".to_owned(),
    }
  }
}
// let light = TrafficLight::new()

ruststruct添加方法是比较简单的,回想下在TypeScript你是如何将一个方法添加到class上的,就比如一个getState

class TrafficLight {
  color: string;
  constructor() {
    this.color = "red";
  }
  getState(): string {
    return this.color;
  }
}

const light = new TrafficLight();
console.log(light.getState());

TypeScript中,默认情况下添加到class中的方法是public的并且是添加到原型链上,所有实例都可以访问到。但在rust中,任何impl中的函数默认都是private的。为了创建一个方法,你需要指定第一个参数是self。使用self时不必每次都指定它的类型,如果你写&self,那么类型默认就是Self的引用,当你写self时,类型就是Self。在一些公共库里你会见到更奇怪的self类型,不过这是后话了

注意:你或许会注意到有时候你拿到了某个struct的实例而且你也知道它具备方法,但是你就是无法访问到,通常有两种原因:一种是你想访问的是一个trait方法,但是没有导入,你需要导入这个trait(例如use [...]::[...]::Trait;);第二种原因是你的实例需要用指定类型来包裹,如果你看到了类似这样的一个函数:n work(self: Arc<Self>),那么你可以用Arc包裹该实例然后访问.work()

rust中我们这样实现getState()方法:

pub fn get_state(&self) -> &String {
  &self.color
}

该方法接收self的引用,返回的数据类型是&string,返回的实际值是内部的color属性的引用,完整代码如下:

&self vs self

struct的方法第一个参数可以是引用的self,也可以是有所有权的self,但是有所有权就意味着在调用方法时调用方必然会失去所有权,也就是丢失了实例,我们可以试试将&self改为self

ub fn get_state(self) -> String {
  self.color
}

然后调用两次:

fn main() {
  let light = TrafficLight::new();
  light.get_state();
  light.get_state();
}

编译的时候会发现无法通过编译,rust会告诉你你使用了一个被移除的内容:

error[E0382]: use of moved value: `light`
  --> crates/day-9/structs/src/main.rs:4:18
   |
2  |   let light = TrafficLight::new();
   |       ----- move occurs because `light` has type `TrafficLight`, which does not implement the `Copy` trait
3  |   println!("{}", light.get_state());
   |                        ----------- `light` moved due to this method call
4  |   println!("{}", light.get_state());
   |                  ^^^^^ value used here after move
   |
note: this function takes ownership of the receiver `self`, which moves `light`
  --> crates/day-9/structs/src/main.rs:25:20
   |
25 |   pub fn get_state(self) -> String {
   |                    ^^^^

For more information about this error, try `rustc --explain E0382`.

第一次调用的时候就丢失了所有权,这可能有点难以理解,这是你在JavaScript中从未接触过的。我们可以从以下几个方面谈谈:

  • 当你创建了一些数据并将他们进行转化,确实是将所有权转移给了新的数据
  • 对象的销毁通常是与实例的创建一起完成
  • builder patterns或链式的API,你可以使用一个有所有权的mute模式的self,并且返回它,以便其他方法可以链式使用

除此之外还有其他一些场景,甚至是需要一些对self的不一样的思考,后续我们会介绍到

Mutating state

目前我们创建的TrafficLight可用性还不是很强,比如不能改变color。在rust中所有的变量默认都是不能修改的,也包括self,我们需要将这个方法标记成需要一个可修改的self,一开始我们可能会想到这么写代码:

pub fn turn_green(&self) {
  self.color = "green".to_owned()
}

然而rust会报出如下错误:

error[E0594]: cannot assign to `self.color`, which is behind a `&` reference
  --> crates/day-8/structs/src/main.rs:32:5
   |
31 |   pub fn turn_green(&self) {
   |                     ----- help: consider changing this to be a mutable reference: `&mut self`
32 |     self.color = "green".to_owned()
   |     ^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be written

For more information about this error, try `rustc --explain E0594`.

实际上我们需要的是可变引用,需要改写成&mut self

pub fn turn_green(&mut self) {
  self.color = "green".to_owned()
}

同时我们还需要将TrafficLight的实例改为可变的,不然rust会继续报错,在main()中我们这样修改:

let mut light = TrafficLight::new();

现在完整的代码如下:

fn main() {
    let mut light = TrafficLight::new();
    println!("{:?}", light);
    light.turn_green();
    println!("{:?}", light);
}

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: String,
}

impl TrafficLight {
    pub fn new() -> Self {
        Self {
            color: "red".to_owned(),
        }
    }
    pub fn get_state(&self) -> &str {
        &self.color
    }
    pub fn turn_green(&mut self) {
        self.color = "green".to_owned()
    }
}

输出结果为:

[snipped]
TrafficLight { color: "red" }
TrafficLight { color: "green" }

枚举(Enums)

如果你像我一样,不喜欢看到"red""green"这类字符串,我们可以把他们归纳到一起,用枚举来实现

把我们的字符串归纳到枚举中,在TypeScript中我们可以这么写:

class TrafficLight {
  color: TrafficLightColor;

  constructor() {
    this.color = TrafficLightColor.Red;
  }

  getState(): TrafficLightColor {
    return this.color;
  }

  turnGreen() {
    this.color = TrafficLightColor.Green;
  }
}

enum TrafficLightColor {
  Red,
  Yellow,
  Green,
}

const light = new TrafficLight();
console.log(light.getState());
light.turnGreen();
console.log(light.getState());

输出结果是:

0
2

TypeScript中枚举值默认是数字,不过你也可以将它们改成字符串:

enum TrafficLightColor {
  Red = "red",
  Yellow = "yellow",
  Green = "green",
}

rust中,枚举也是相当简明:

enum TrafficLightColor {
  Red,
  Yellow,
  Green,
}

然后将我们的代码改成:

fn main() {
    let mut light = TrafficLight::new();
    println!("{:?}", light);
    light.turn_green();
    println!("{:?}", light);
}

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 get_state(&self) -> &TrafficLightColor {
        &self.color
    }

    pub fn turn_green(&mut self) {
        self.color = TrafficLightColor::Green
    }
}

enum TrafficLightColor {
    Red,
    Yellow,
    Green,
}

此时我们会看到VS Coderust-analyzer已经给你提示出错误,这是因为TrafficLightColor不可打印不可调试导致TrafficLight也是不可打印不可调试的,我们需要像对待TrafficLight那样给TrafficLightColor也赋予DebugDisplay性质,在枚举前面加上#[derive(Debug)]

#[derive(Debug)]
enum TrafficLightColor {
    Red,
    Yellow,
    Green,
}

此时我们已经解决了一个报错,接下来我们处理Display问题,我们首先写出如下代码:

impl Display for TrafficLightColor {}

此时VS Code会报出"cannot find trait Display in this scope",此时点击[快速修复]按钮,可以看到VS Code给出的修复建议

image.png

image.png

选择Import Display下的std::fmt::DisplayVS Code会自动在代码顶端添加use std::fmt::Display;,不过此时我们会遇到一个更长的红色下划线

image.png

继续点击[快速修复]按钮,选择Implement missing members,然后你会得到一个模板:

impl Display for TrafficLightColor {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        todo!()
    }
}

注意:上面的 todo! 宏很有用处,详见文档

匹配表达式允许我们将表达式的结果与模式进行匹配,下面的代码将TrafficLightColor的可能值与其自身相匹配,以生成一个适当的显示字符串:

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)
    }
}

注意:write! 也是一个宏,可以用于格式化并返回一个ResultResultOption类似,我们后面会介绍到,现在你只需要将write!当成是print!来用就行

现在,我们的完整代码如下:

use std::fmt::Display;

fn main() {
    let mut light = TrafficLight::new();
    println!("{:?}", light);
    light.turn_green();
    println!("{:?}", light);
}

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 get_state(&self) -> &TrafficLightColor {
        &self.color
    }

    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)
    }
}

输出结果是:

[snipped]
Traffic light is red
TrafficLight { color: Red }
TrafficLight { color: Green }

总结

Structsenumsrust中是非常重要的,rust中的enums远比TypeScript更富有表现力,可以表现出更复杂的数值。与此同时,match表达式也十分具有力。你会经常一起用到enumsmatch表达式,不要忽视,多花时间,这会帮助你构建rust编程的思考方式

更多