前言
在上一篇文章中,我们介绍了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
添加方法和更多内容