昨天我们接触到了rust
里的面向对象编程知识
今天继续往下学
Patterns
)和匹配(Matching
)模式是rust
中的一种特殊语法,它被用来匹配类型的结构,既复杂又简单(呃....)。
使用模式搭配match
表达式或者其它构造类型能改善我们对程序的控制流(control flow
)的控制。
一个模式一般由以下某几项组合而成:
Literals
:字面量,比如123这样的数字等,会直接硬编码进binary
里面 ;Destructured arrays, enums, structs, or tuples
: 解构数组、枚举、结构体或者元组;Variables
:变量;Wildcards
:通配符;Placeholders
:占位符;它们都可以用来表示某个数据的样子/状态,比如Some(Color::Red)
、x
或者123
。
然后我们可以匹配它们,判断这些个数据在匹配的上下文是否是正常的。
其实我们之前就学过了匹配最常用的match
表达式,或者if let
等。
接下来将会深入的了解模式和匹配。
Patterns
)其实看到上面模式的组成部分,你就应该已经知道了我们之前就一直在用模式了,比如声明一个变量let x = 123;
这个变量就是一个模式。
match arms
这个不知道咋翻译,干脆不翻译了。
我们来回顾下match
的用法
PATTERN => EXPRESSION
就是一个arm
,而箭头左边就是一个Pattern
,表示被匹配的对象,既模式。
稍微有那么一丁点抽象,我们直接来看个例子
我们match
了x
这个Option
类型的枚举。
根据前面模式的组成部分,枚举也是一个pattern
。
另外回顾下之前我们学的match
,如果一个枚举类型存在多个字段,那么我们在match
匹配的时候就需要把这些字段都匹配了才行。
当然,也不全是这样,我们还学了_
占位符。_
占位符仅仅表示一个占位符而已,表示其它场景我们不在乎,它不能作为变量,如果你尝试去使用这个通配符就会报错。
比如:
我们等会将接触更多的和_
占位符相关的知识。
除了下划线通配符,我们之前还学了if let
的用法。
if let
条件表达式我们之前学过它是用来简写,表示我们只在乎某个场景,其它场景都不处理。
比如:
其实if let
还可以接else
分支的,我们再来看个例子
我们模拟了下根据最喜欢的颜色、今天是否是星期二(今天几号?16号咯~)以及用户的年龄来返回用户应该会使用什么背景颜色。
优先判断用户最喜欢的颜色,然后是日期,最后才是判断年龄。
目前模拟的结果应该是purple
,打印看一下。
正常。
可以看到不仅有if let
还有else if let
和else if
,它们可以混用的。
混用这个词不是很贴切,其实在rust
中else
后面可以直接接一个表达式,所以看起来就像是在混用。
这里还有一个点需要说下,let Ok(age) = age
,前面我们声明过了age
这个变量,现在我们又重新声明了一个age
,值是Ok(age)
里的age
。 所以后面age > 30
这个并不会报错。
使用if let
还挺方便的,不过有一个点需要注意,那就是我们没有把匹配的所有情况都列举出来,并且编译器不会报错,这样就有可能存在业务相关的bug
。
while let
条件循环类似if let
,while let
会一直循环直到没有东西可以匹配为止。
直接来看例子
来看下pop
源码
可以看到返回的是Option
的类型,并且每次的len
都自减1,当为0
的时候返回None
。
那么while
就会执行len
次。
表现符合预期。
for
循环中遍历的元素就是一个模式,比如
这里的index
和value
就是模式,它们其实算是变量,匹配v
里面的元素。
而这个例子中我们还使用了解构,把enumerate
返回的结构体Enumerate
给解构了
至于这里为啥是个元组
我想应该是因为这里。
扯远了,回到我们的代码
let
语句因为声明了变量,自然也是一个模式匹配。
比如:
这个x
就是一个模式。
它可以抽象成这样
我们可以来看个更能说明这是一个模式匹配的例子
元组的解构赋值,x
匹配到1
,y
匹配到2
,z
匹配到3
。
元组的解构是一视同仁的,你必须把所有的元素都解构出来才行,不然编译器会直接报错。
不过我们后面会了解到一种方式可以忽略某些元素,只关注我们想要的元素,毕竟这太不人性化了。
基本上有变量的地方就有模式。函数的参数也算是一种变量,它匹配了传入的数据并bind
这个数据。
我们来看个例子
print_coordinates
这个方法接受一个元组的引用,那自然就可以解构了。x
匹配到了元组的第一个元素,y
匹配到了元组的第二个元素。
闭包同理。
Refutability
): 一个模式可能匹配失败既然有可辨驳性(refutability
),那自然也有不可辩驳性(irrefutability
)。
如果一个模式可以匹配任何的值,那这个模式就是不可辩驳的,这个词通俗的说就是:不可能失败的。
比如:
变量声明是不可辩驳的,它能匹配任何的值。
当然,编译器报错提示类型不对这个属于其它情况,这个需要我们手动去填写类型就可以解决,并不属于模式匹配失败的场景。
前面提到的函数参数、let
语句以及for
循环都是不可辩驳的。 为啥呢?
因为它们的模式理论上都是可以接收任何的值的。
那什么情况是可辨驳的呢?
直接来看下例子
如果x
变成了None
,那它就匹配失败了。
所以if let
和while let
都是都可能可辨驳的,当然,它们也有可能不可辩驳,取决于写代码的你。
毕竟它们的模式上下文中有条件表达式,自然就可能匹配失败。
通常情况下,我们并不需要担心模式的可辨驳以及不可辩驳。
不过最好还是熟悉下,到时除了错误方便排查。
来看两个例子加强训练
我们使用了let
语句,它是不可辩驳的,但是我们居然还加上了Some
,太牛角尖了。
不可辩驳搭配可辨驳,直接量子力学。
编译器可不会让我们这么量子力学,直接乱棍打死。
这样你就不能写这种前后矛盾的语句了。
上面的代码可以改成if let
来通过编译
然后我们再来看个反过来的例子:可辨驳的搭配不可辩驳的。
这样编译器不会报错,毕竟不可辩驳也可以算可辨驳的一个子集。
但是会给一个警告
Pattern
)语法(Syntax
)前面我们说了好几种模式匹配的写法,我们将在这一章节来学习什么场景选择哪种更合适。
匹配字面量我们可以直接用match
,比如
当箭头左边的模式是一个字面量的时候,并且是一连串的字面的的时候,用match
就很合适了,比如字面量\枚举这些确定类型的。
在其他语言中大概率会是switch
的写法(以js
为例),用match
优雅很多,相当于把switch
改成了对象匹配{ ... }[xxx]
这样。
这个老熟人了,它是不可辩驳的,因为理论上它能接收任何的值。
如果还使用上面的match
来写的话,就不太好了。
match
表达式的{}
是一个新的作用域。 如果你在这个作用域中声明一个同名变量,那在当前作用域中它就会覆盖外层作用域的同名变量。
比如:
我们来运行下
可以看到里面作用域的x
的值和外面作用域x
的值是不一样的。而y
则是外层作用域的x
的Some
保存的值。
这样的可阅读性就差很多了。不过一般情况下我们都不会这样写,虽然有作用域保护,但是会产生歧义。
那么要怎么改呢?我们之后会学到一个match guard conditional
,匹配条件守卫。
现在先绕过。
Multiple Pattern
)当我们匹配字面量的时候,我们用match
,然后每个arms
都需要写出来。这时就存在一个问题,当某两个arms
需要做的事情是一样的时候就会存在重复代码,虽然我们可以抽函数出来,但是这样就会把问题搞得复杂了,我们需要有类似switch
允许多个条件之间不写break
来复用同一块逻辑的功能。
这个时候多重模式就可以登场了
我们直接来看下例子
使用|
把多个需要执行相同逻辑的模式分隔开。
..=
匹配一个范围之前我们也用过..=
这个符号,在我们创建一个range
的时候。
这也可以用在模式匹配中
如果这个值是在1~5
之间,那它就会被匹配到。
另外字符的范围也是可以匹配到的
没想到吧~
对于前端开发来说,解构这个词大家应该都很熟悉了,这里就不多说了。
而在rust
中我们已经接触过元组的解构了,实际上struct
和enum
也都是可以解构的。
先来看下struct
的
有点类似前端对象解构时允许的命名空间,这里用a, b
匹配x, y
。
实际上更常用的是这样:
直接使用原来的字段。
这也是一种匹配,模式是x, y
。
另外解构也能被拿来match
匹配,不过仅限于字面量的解构,为什么呢?因为解构的过程产生两个新变量,这个问题就回到匹配命名变量使用match
了。
我们解构了x, y
,当y
为0
的时候,匹配打印输出在x
轴,同理当x
为0
的时候打印输出在y
轴。
这里需要注意的是这俩arm
各自只产生了一个变量,另一个则是字面量。
然后我们来看下解构枚举,直接来看下例子
这个直接一目了然了,就不多说了。
然后我们再来看个套娃的枚举
我们的ChangeColor
变体的值是一个Color
枚举,然后再match
,我们解构了ChangeColor
的值的变体的值。
可以看到一旦套娃过多,这里match
的arm
就会变得非常多。
我们还可以混搭它们的解构
structs
和tuples
前面套娃枚举能够解构,那么其它套娃场景也是可以解构的。
比如:、
我们解构了一个元组,这个元组第一个元素也是元组,第二个则是结构体。然后我们都给它们解构了。
前面说过解构一个元组需要把元组里的所有元素都解构出来才行,但有时候这个元组巨长,我们并不想写这么多东西,这就很不人性。
我们在match
的时候可以通过_
占位符省略部分arm
,而这个占位符其实也能用在其他地方,效果也是省略。
我们先来看个例子
我们省略了第一个参数。
和match
中一样,这里的_
不能被当做变量,只是占位而已。
我们省略了第一个参数,看起来好像有些大病。
实际上还有有用的,因为当我们在实现一个trait
的时候,重写实现这个trait
里的方法时,我们并不需要某个参数,这个时候就可以用占位符了。
我们还可以忽略多个参数,比如
解构的时候我们忽略了第二、第四个参数。
我们还可以忽略值,比如:
我们匹配这个元组,只需要这俩元素都是有效的就行了,其它我不关注。
另外我们还可以用下划线开头来存储某个未使用变量,让编译器不会提示。
y
会被编译器警告,而_x
不会。
不过需要注意一点,_
和_xxx
是有区别的,_
只是一个占位符,而_xxx
还是一个变量,来看个例子
这样是会报错的。
s
里的值的所有权被转移到_s
里了,这里再打印s
自然就会报错。
而当我们把_s
改为_
之后就不会报错了。
_
占位符挺方便的,但是有些场景还不够方便,比如如果一个元组有20个元素,我们只需要前几个,而这时解构就不可能了,而直接用点下标获取也需要写一堆重复代码,这个时候我们就需要另一种省略方法。
..
忽略剩余部分。直接来看下例子
这里只解构了x
,按照之前的写法,我们得这样写{ x, y: _, z: _ }
,现在我们直接..
就好了。
然后我们再来看下解构元组里的用法
我们忽略了中间的参数,只解构第一个和最后一个。
不过这里有一点需要注意,那就是解构的元素的位置是需要确定的,比如下面这种用法就会有问题
这个second
我们压根不知道是哪个位置的元素,它可能是第二个第三个第四个。
Match Guards
match guards
是一个特殊的if
条件,跟在match
的arm
后面,我们直接来看下例子
和之前的例子不同的地方在于我们Some(x)
之后还加了一条条件表达式if x % 2 == 0
,并且在下一个arm
我们还是在匹配Some(x)
。
这个条件表达式就是一个match guard
,在匹配到arm
之后又加了一层保险,还得是2
的倍数才行。
这就很符合“守卫”这个词了。
这么做就使得match
表达式变得灵活了,不过随之而来的缺点就是编译器不会再穷举所有的可能性,毕竟如果存在俩arm
长得一样的按原来的规则早就乱棍打死了。
还记得我们在匹配命名变量这一小章节留的小坑吗?
我们来用match guard
改下
我们用n
代替原来的y
,然后加上守卫if n == y
。这样就能重命名变量了而不会shadow
外层作用域的y
,
现在的可阅读性高了很多。
只要在match
表达式里的,就可以用守卫,我们之前学的比如多重模式就可以使用这个守卫再二次筛选
虽然这么写有些大病。
这个arm
看起来像是4 | 5 | (6 if x == 4)
,实际上并不是,更接近于(4 | 5 | 6) if x == 4
。
我们直接来看下例子
然后打印下结果
我们先来解释下代码,Message
这个枚举的Hello
变体存储的值类型是一个struct
。
我们用match
匹配这个struct
,第一个arm
中我们解构了id
,命名为id_variable
,但是我们后面还加上了@
以及一个范围3..=7
。
而根据我们的输出结果,这个id_variable
的值是5
也就是我们匹配的实例的id
的值。
把这个arm
和下面第二第三个arm
对比,相信大家都已经猜到这个@
操作符是用来干嘛的了,就是用来把匹配的值赋值给变量的。
之前我们匹配解构的字面量或者范围的时候我们没有办法使用这个匹配的值,因为它并不是一个变量。
而现在,基于@
操作符,我们可以即解构了字面量又可以用这个数据。
这一章节我们主要是学匹配/解构的方式,以及模式和匹配的概念。
相对最近学的几章来说比较简单,也算是一个过渡吧,下一章的知识点是所有章节最多的。
发布于 2023-01-08 17:21・IP 属地广东