前言
在前面的文章中,我们详细的了解了关于语法、类型相关的知识,本篇是一个新的篇章,在前面的文章中都没有涉及过,本篇的主题就是“模式”,在本篇文章中,会详细讲解关于Dart模式的知识,从官网的内容出发,也会加上一些个人使用的小技巧和一些习惯。
什么是模式?
要学习模式,那么首先我们就要知道,什么是模式。在Dart官网中,对于模式的定义是这样的:
Patterns are a syntactic category in the Dart language, like statements and expressions. A pattern represents the shape of a set of values that it may match against actual values.
从这段话我们可以知道,模式其实就是Dart中的一个语法种类。它主要用来表示一组值的形状还可以与实际的值进行匹配。光这么说可能不太具体,后面会给出详细的实例,现在只需要记得,Dart中的模式是一种特殊的语法。
模式可以做什么?
模式匹配
说到模式可以做什么,那我们首先就要讲到“模式匹配”,因为在前面说了,Dart中的模式是可以与实际的值相匹配的,所以模式匹配也是模式最根本的作用,下面我们来看一个示例,来看一下模式匹配是怎样使用的,以及它有什么样的效果。
void main(List<String> arguments){
var number=1;
String res;
switch(number){
case 1:
res='one';
}
}
看到这,你可能就恍然大悟了,这个所谓的模式匹配,不就是分支表达式吗,有什么神奇的,需要专门写一篇博客来介绍吗?
那这样的写法呢?
void main(List<String> arguments){
const a = 'a';
const b = 'b';
const obj=['a','b'];
switch (obj) {
// 当一个list中只有两个元素,并且两个元素的值分别与常量a和常量b的值相等时,才会匹配成功
case [a, b]:
print('$a, $b');
}
}
这样的写法你还熟悉吗?这种就是模式匹配的中的一种,叫做常量模式匹配,除了常量模式匹配还有很多种模式匹配,模式匹配的不同,主要是根据模式的类型来进行的,不同类型的模式会进行不同类型的匹配,匹配的规则和做法也不一样。
就像上面两段代码,虽然都是常量模式匹配,但是还是有很大的不同的,我们在这里展开讲讲,第一个没有什么好说的,大家都能看懂,就是直接匹配传入的值是否为1
,我们重点讲讲第二个匹配,虽然也不难,但是能帮助我们理解模式匹配的工作流程。
在第二段代码中,我们有这样一个模式[a,b]
,这是一个完整的模式,他可以分为一个外部模式和两个内部模式,其中[]
就是外部模式,它先匹配一个只有两个元素的list,而a
和b
则是内部模式。模式匹配的规则就是,必须得先等外部模式匹配完成之后,再到内部模式上递归的匹配,为什么要递归呢?
因为外部模式和内部模式都是相对的,就比如你可能会遇到这种模式(a,b,[c])
,在这里()
是a
、b
、[c]
三个内部模式的外部模式,但是对于c
来说,[]
又是它的外部模式,所以外部模式和内部模式是一个相对关系,而不是绝对关系。而这种外部模式和内部模式处于相对关系的情况下,要完整的匹配整个模式,就需要递归的对一个大的外部模式中的所有模式进行匹配。
看到这,读者对模式匹配应该有一个大致的了解了,后面会有例子再帮助读者更深入的掌握模式匹配和模式,接下来我们再来看看模式的另外一个作用。
解构
解构是一个在许多现代编程语言中都存在的特性,那么Dart当然也不能例外。那么解构到底是什么呢?为什么现代的编程语言都支持呢(其实不止现代的语言支持,一些老的语言也通过新的手段间接的支持了类似的操作,比如C++17的结构化绑定,和解构的作用就非常类似)?
解构说白了,就是将一个对象或者集合中可以访问的数据中的某一部分提取出来,然后将该部分赋予一个变量或者直接使用,这就是解构的主要作用,我们来看一个例子,就能很好的理解解构了:
var numList = [1, 2, 3];
var [a, b, c] = numList;
print(a + b + c);
这段代码将一个list解构到了a
、b
、c
三个变量上,它们的值分别为1,2,3。这是我们常用的一种方式,将集合中的数据提取出来,但是关于List我们更常用的可能还是下面这种写法:
var numList = [1, 2, 3];
var [first,..., last] = numList;
print(first+last);
这段代码的意思就是将list中的第一个和最后一个结构出来,赋值给first和last,当我们需要访问一个list的头尾时,使用这种方式可以方便的一次访问头和尾,不用去访问两次,并且通过这种方式,我们不止能一次访问头尾,也能一次访问List中的第一个、第二个加上倒数第一个、倒数第二个,要进行这些操作只需要在解构的时候声明不同位置的变量即可,我们来看一下例子:
numList=[1,2,3,4,5,6,7,8];
// 这样就可以将List中的第一个和第二个解构出来了,那么倒数第一和倒数第二也是一样的。
var [first1,second,...]=numList;
而这种解构方法最主要的就是中间的...
,这个粗浅的理解就是当做省略号,省略掉不想要的部分。
注意:上面这些写法要求你的list有足够数量的元素,否则会报错
关于解构还有一种用法就是与模式匹配相结合,代码长这样:
switch (list) {
case ['a' || 'b', var c]:
print(c);
}
这段代码,在我们传入的list拥有两个元素,并且第一个元素的值是’a’或者’b’时,才会匹配成功,比如这样的list就会匹配成功:
var listA = ['a', 1]; // 成功
var listB = ['b', 2]; // 成功
var listC = ['c', 3]; // 失败
以上两个作用就是我们使用模式最常用的了,但是模式的作用并没有这么简单,因为以上的操作不用模式,用其他的方式来操作也没有很困难,接下来我们再来看看模式真正的威力!
模式的使用
在这部分我们会讲除了上面两种,其它关于模式的使用方法,比如变量声明时解构模式,变量赋值模式,switch`语句和表达式的不同使用,等等。
首先我们来看关于变量声明时解构模式的内容,其实就和前面的解构一样,语法如下:
var (a, [b, c]) = ('str', [1, 2]);
这个就是结合了模式匹配和解构的作用,通过递归的匹配模式,然后将值赋予给变量,通过前面的学习,相信对于这一块的理解还是非常简单的,我们再来看另一个例子,就是变量赋值模式,语法如下:
var (a, b) = ('left', 'right');
(b, a) = (a, b);
通过这种模式 ,我们可以在不定义新变量的情况下,将一个对象或者集合,解构给匹配的模式中的变量,比如这里就是将a和b进行了一个交换。
这两种使用方式都是非常简单的使用方式,读者们自己看两眼也就会了,就不再细说,接下来就来讲讲模式匹配中真正的大杀器之一,switch
表达式和switch
语句。
switch语句
在说这个之前,我们先来了解一个前置知识,这个知识不仅能帮助我们学习switch
语句也能帮助我们学习其它模式匹配使用方式,这个知识就是关于case
子句的。
在官方文档上,关于case
子句是这么说的,所有case
子句都可以包含一个模式。这个规则不仅使用在switch
语句和表达式上,也能用于if case
语句上。并且,最主要是,case
子句包含的模式是一个可辩驳的模式。
这里又引申出一个概念,可辩驳模式和不可辩驳模式,简单的来说就是**可辩驳模式允许匹配失败,而不可辩驳模式必定匹配成功。所以不可辩驳模式是有穷尽的,而可辩驳模式可以是没有穷尽的。**这么说可能有点抽象,但是先记住,后面会给出例子帮助读者理解。
了解了这个概念,我们就来看一下switch语句怎样使用,我们来解读一下官网的例子:
switch (obj) {
case 1:
print('one');
case >= first && <= last:
print('in range');
case (var a, var b):
print('a = $a, b = $b');
}
在这段代码中,每个case子句都包含一个模式,obj传入进来的时候会依次匹配所有的模式,只有当obj成功匹配某个模式,才会执行case子句后面的操作,如果所有模式都没有匹配到,那么就什么都不做,这种模式就是一种可辩驳模式,因为允许匹配失败,而且我们可以看到,case子句中包含的模式可以是各种各样的,可以是解构,可以是常量模式匹配,甚至可以是一个逻辑表达式匹配,就像第二个case一样,只有obj>=firs
并且obj<=last
,那么才会匹配到该模式
所以switch语句的作用就是,可以放置多个模式在内部,传入进来的表达式会依次匹配内部的所有模式。
这里有个很重要的注意事项,常量模式匹配和解构我们要分清楚!!!!
switch (obj) {
case 1:
print('one');
// 这里没有声明新的变量 并且first和last是一个常量、常量、常量,重要的事情说三遍
case >= first && <= last:
print('in range');
//这里是解构,声明了新的变量,或者使用一个可变的变量作为模式匹配的内部模式。
case (var a, var b):
print('a = $a, b = $b');
}
switch表达式
这个是作者比较喜欢使用的模式匹配之一,因为和Rust的模式匹配很像,用起来十分顺手,它和switch语句的主要区别就是,switch表达式会返回一个值而switch语句则没有返回值,并且switch表达式是一个不可辩驳的模式而switch语句是一个可辩驳的表达式,也就是说switch表达式要求是穷尽的,必须匹配成功,至于什么是穷尽,我们先给出一个例子来看一下什么是穷尽和不可辩驳以及switch表达式的基本语法:
const number = 1;
var res = switch (number) {
1 => 1,
_ => -1
};
以上这段代码就是switch表达式的基本用法,我们可以看到和switch语句有很大的不同,首先,我们使用了一个变量来接收switch表达式的值,其次,我们没有使用case语句来包含模式,最后,我们使用了一个 _
的模式,这个模式代表,没有匹配到其他模式的时候就返回该模式的值,并且这个是必须的,否则编译器就会报错,因为switch表达式是一个不可辩驳模式,它匹配必须成功,也就是说该表达式必须有值,也正因为它是一个表达式,表达式怎么能没有值呢,对吧?
switch表达式和switch语句虽然在语法上有所不同,但是在模式的写法上是一致的,也就是说你可以这么写:
var res = switch (listA) {
['a' || 'b', var c] => c,
_ => -1,
};
也可以这么写:
const num = 100;
var res = switch (num) {
> 100 && < 200 => true,
_ => false
};
总之就是在switch语句中的模式,你在switch表达式中都可以使用,当然解构也不例外。不过我们在使用switch表达式的时候要注意如下几点:
- 每个switch表达式都必须是穷尽的。
- switch表达式中所有模式的返回值必须是同一类型。
- switch表达式中所有模式匹配成功能够执行的代码必须是一个只有一行的表达式,也就是说不能像switch语句中那样使用一个大语句块,里面可以执行多行代码(这一点是作者觉得设计不足的地方,如果能像rust的模式匹配一样那就太完美了)。
循环中解构
Dart提供了在for in
循环中对临时变量进行解构的方式,具体用法如下:
for (var MapEntry(key: key, value: count) in hist.entries) {
print('$key occurred $count times');
}
我们可以将一个entries类型解构为一个key和一个value,并且在这块dart还提供了另一个语法糖,那就是当要解构的变量和解构之后复制的变量同名时,可以省略要解构的变量不写,具体语法如下:
for (var MapEntry(:key, value: count) in hist.entries) {
print('$key occurred $count times');
}
if case语句
这也是一种模式匹配的用法,就和Rust中的if let
差不多,具体使用方法如下:
if (pair case [int x, int y]) {
print('Was coordinate array $x,$y');
} else {
throw FormatException('Invalid coordinates.');
}
这里的case子句和switch语句中的case子句使用方式相同,也支持所有种类的模式,包括解构,只是不同的是,在if case
语句中,当模式匹配成功时,会返回一个true,匹配失败时就返回false。
总结
本篇文章我们详细的学习了关于Dart中模式的知识,介绍了什么是模式,怎样使用模式,模式最主要的两大作用解构和模式匹配,可辩驳模式和不可辩驳模式分别是什么等等,详细读者学习完本篇博客,对于Dart的模式也有了一个比较全面的理解,不过由于作者水平有限,并没有写的十分深入,文章中也可能存在错误还请见谅。我们在下篇文章中将会继续学习关于模式种类的知识。