前言
在上一篇文章 《写给前端看的Rust教程(8)语言篇[中]》我们介绍了如何创建struct
,我们已经知道了rust
中的struct
与JavaScript
中的class
有相似之处,本文我们将继续学习struct
的方法以及match
表达式
注意:我们使用
TypeScript
会开始多于JavaScript
,你需要安装ts-node
(npm install -g ts-node
)来运行文中的TypeScript
示例
struct添加方法
我们昨天增加的new
方法类似TypeScript
中的static
函数,你可以通过名字直接调用而不必实例化:
impl TrafficLight {
pub fn new() -> Self {
Self {
color: "red".to_owned(),
}
}
}
// let light = TrafficLight::new()
给rust
的struct
添加方法是比较简单的,回想下在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 Code
和rust-analyzer
已经给你提示出错误,这是因为TrafficLightColor
不可打印不可调试导致TrafficLight
也是不可打印不可调试的,我们需要像对待TrafficLight
那样给TrafficLightColor
也赋予Debug
和Display
性质,在枚举前面加上#[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
给出的修复建议
选择Import Display
下的std::fmt::Display
,VS Code
会自动在代码顶端添加use std::fmt::Display;
,不过此时我们会遇到一个更长的红色下划线
继续点击[快速修复]按钮,选择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! 也是一个宏,可以用于格式化并返回一个
Result
,Result
和Option
类似,我们后面会介绍到,现在你只需要将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 }
总结
Structs 和 enums 在rust
中是非常重要的,rust
中的enums
远比TypeScript
更富有表现力,可以表现出更复杂的数值。与此同时,match
表达式也十分具有力。你会经常一起用到enums
和match
表达式,不要忽视,多花时间,这会帮助你构建rust
编程的思考方式