前言
在上一篇文章中,我们介绍了JavaScript和rust之间的一些基础差异,并最终以对标JavaScript数组的Vector作为结束。在JavaScript中数组是非常关键的数据结构,不过和万能的Object相比就逊色不少。在JavaScript中对象可以是个map、dictionary、tree、基础class、一个实例、工具函数的集合……,接下来我们会拆解JavaScript中的对象概念并对应到rust中
注意:随着我们不断前进,后面
TypeScript使用的会比JavaScript更多,你需要安装ts-node(npm install -g ts-node)来运行文中的示例
Maps vs Objects
在ECMAScript 6之前,JavaScript没有Map,Map功能一直是依托Object来实现的,这导致不少程序员将Object视为Map,所以我们首先要澄清二者之间的区别。JavaScript中的Map本质是key/value键值对,将一个数值value存储在某个key之下,然后通过这个key来访问这个数值。JavaScript中的Object是一种数据结构,用属性值来存储和访问数据。在JavaScript中Object和Map可以交换着用,但在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,这是怎么回事呢?
让我们做个分析:
- 首先,
HashMap的key和value都是字符串字面量,所以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,在这里Self是TrafficLight的引用。我们可以通过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)
}
}
上手代码实现了一个trait(std::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中获得类似JavaScript中class行为的能力,这需要点时间来适应
trait是强大的,是struct的支柱。数据和行为的分离很重要,但需要一些实践来适应,明天我们将向struct添加方法和更多内容