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

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

原文:24 days from node.js to Rust

前言

在上一篇文章中,我们介绍了JavaScriptrust之间的一些基础差异,并最终以对标JavaScript数组的Vector作为结束。在JavaScript中数组是非常关键的数据结构,不过和万能的Object相比就逊色不少。在JavaScript中对象可以是个mapdictionarytree、基础class、一个实例、工具函数的集合……,接下来我们会拆解JavaScript中的对象概念并对应到rust

注意:随着我们不断前进,后面TypeScript使用的会比JavaScript更多,你需要安装ts-nodenpm install -g ts-node)来运行文中的示例

Maps vs Objects

ECMAScript 6之前,JavaScript没有MapMap功能一直是依托Object来实现的,这导致不少程序员将Object视为Map,所以我们首先要澄清二者之间的区别。JavaScript中的Map本质是key/value键值对,将一个数值value存储在某个key之下,然后通过这个key来访问这个数值。JavaScript中的Object是一种数据结构,用属性值来存储和访问数据。在JavaScriptObjectMap可以交换着用,但在rust中就不行了,要分别对待

当你想要一个value是相同类型的key集合时,你需要的是Map;当你想要一个包含已知属性的对象时,你需要的是一个更加结构化的数据类型

Map有时候指的是一种概念,每个语言都有很多种不同的实现,下面我们要介绍的是HashMap

从 Map 到 HashMap

为了存储任意key值的数据,我们需要 HashMap,虽然这不是唯一的选择,不过现在我们还不必考虑那么多

TypeScript中我们通过如下的方式创建一个Map

const map = new Map<string, string>();

map.set("key1", "value1");
map.set("key2", "value2");

console.log(map.get("key1"));
console.log(map.get("key2"));
复制代码

rust中,我们可以这么写:

use std::collections::HashMap;

fn main() {
  let mut map = HashMap::new();
  map.insert("key1", "value1");
  map.insert("key2", "value2");

  println!("{:?}", map.get("key1"));
  println!("{:?}", map.get("key2"));
}
复制代码

打印结果是:

Some("value1")
Some("value2")
复制代码

Some()、None、Option

上面的源码很相似,不过在rust的打印结果里我们看到了陌生的东西,那就是some()some()实际上是 Option enum 的一个变体,Option是另一种表达“无”的方式

注意:JavaScript中没有enum的概念,TypeScript是有的,当然rust中的enum更巧妙

我们以后会展开讲解enum,不过现在你只需要知道Option可以用来表示空和非空,rust中没有undefined,也没有null,取而代之的是Option,它有两个变体:Some()None

注意:我们可以用.is_some().is_none()来检测一个Option;可以通过.unwrap()来获取Some值,不过如果值是None的话代码会出问题,所以我们用.unwrap_or(default_value)代替。更多信息可见 Option

我们可以将代码改成下面的样子,以便让输出更加简洁:

use std::collections::HashMap;

fn main() {
  let mut map = HashMap::new();
  map.insert("key1", "value1");
  map.insert("key2", "value2");

  println!("{}", map.get("key1").unwrap_or(&""));
  println!("{}", map.get("key2").unwrap_or(&""));
}
复制代码

注意:上述代码是我们明确知道Map的数值不是None,所以我们可以使用.unwrap(),但这是一个好的开始,我们尽量还是将代码写的更加严谨些

在上面的代码里我们写成unwrap_or(&"")而非.unwrap_or(""),为什么呢?假如我们把代码改成.unwrap_or(""),则会发现编译器报出如下错误:

error[E0308]: mismatched types
 --> crates/day-8/maps/src/main.rs:9:44
  |
9 |   println!("{}", map.get("key2").unwrap_or(""));
  |                                            ^^ expected `&str`, found `str`
  |
  = note: expected reference `&&str`
             found reference `&'static str`

For more information about this error, try `rustc --explain E0308`.
复制代码

上面的报错让人十分困惑,开头说希望得到&str而实际是str,但后面又说希望是&&str而实际是&'static str。我们在前文讲过字符串字面量的类型是&str,不过我们没提过什么是'static,这是怎么回事呢?

让我们做个分析:

  • 首先,HashMapkeyvalue都是字符串字面量,所以rust会推测HashMap的类型应该是HashMap<&str, &str>
  • 其次,.get()不会返回一个有所有权的数值,而是会返回一个引用的数据,否则如果返回的是一个有所有权的数值,则这就意味着HashMap要放弃改数值的所有权,这等同于是从HashMap中删掉了该数据。当然,返回一个克隆值也不是不可以,但那样会增加额外的消耗,rust不会自动做这种事。所以这里的得到的实际上是一个字符串字面量的引用,&str的引用自然就是&&str
  • .unwrap_or()接收到的值应该是与.get()返回值相同类型,所以应该是&&str&""
  • 'static的含义指的是数据的有效周期贯穿整个应用的运行周期,rust会确保字符串字面量的有效周期与程序一样长,如果看到了&'static str要明白这个指的就是字符串字面量

作者按:编译器最初提到的“希望得到&str而实际是str”是什么意思呢?这个作者也搞不懂,猜测这应该是提示器的一个错误

object、class、struct

rust中的struct使用的场景非常广,当你使用一个struct的方法时,那场景就类似在使用JavaScript中的一个class实例,不过我建议你还是将struct视作是纯粹的数据

TypeScript中我们可以这么实现一个interface

interface TrafficLight {
  color: string;
}
复制代码

类似的,在在rust中我们可以这么做:

struct TrafficLight {
  color: String,
}
复制代码

实例化也是相似的,在TypeScript中是这样的:

const light: TrafficLight = {
  color: "red",
};
复制代码

rust中是这样的:

let light = TrafficLight {
  color: "red".to_owned(), // Note we want an owned String
};
复制代码

TypeScript我们可以使用class来进一步进行丰富,给予初始值并添加方法:

class TrafficLight {
  color: string;

  constructor() {
    this.color = "red";
  }
}

const light = new TrafficLight();
复制代码

rust要想做到这点,需要借助impl

struct TrafficLight {
  color: String,
}

impl TrafficLight {
  pub fn new() -> Self {
    Self {
      color: "red".to_owned(),
    }
  }
}
复制代码

我们增加了一个公共函数new()用于得到一个新的TrafficLight,在这里SelfTrafficLight的引用。我们可以通过TrafficLight::new()调用:

fn main() {
  let light = TrafficLight::new();
}
复制代码

这个代码是起作用的,但是我们无法验证它。你可以试着按如下的代码去打印,但会发现无法编译:

fn main() {
  let light = TrafficLight::new();
  println!("{}", light);
  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)
  }
}
复制代码

上手代码实现了一个traitstd::fmt::Display),现在我们就可以使用println!()看到结果了:

Traffic light is red
复制代码

trait可以有一个推导出的默认实现,这有助你精简代码。如果你的struct所有字段都支持Debug特性,那么你可以通过添加一行#[derive(Debug)]来自动获取该能力:

#[derive(Debug)]
struct TrafficLight {
  color: String,
}
复制代码

完整代码如下:

fn main() {
  let light = TrafficLight::new();
  println!("{}", light);
  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(),
    }
  }
}
复制代码

现在运行如下代码,你会发现两个打印结果都有了:

[snipped]
Traffic light is red
TrafficLight { color: "red" }
复制代码

总结

HashMap是存储和使用键值对的主要工具,后面介绍数组和迭代器时我们还会深入介绍,在 这里 你可以阅读到更多有关信息

struct使你在rust中获得类似JavaScriptclass行为的能力,这需要点时间来适应

trait是强大的,是struct的支柱。数据和行为的分离很重要,但需要一些实践来适应,明天我们将向struct添加方法和更多内容

更多

分类:
前端
标签: