模式的作用:
模式是用来进行匹配和解构的;
- 匹配:
dart
switch (number) {
// Constant pattern matches if 1 == number.
case 1:
print('one');
}
- 解构:
dart
void test1() {
var map = {
'a': 1,
'b': 2,
'c': 3,
'd': 4,
'e': 5,
'f': 6,
'g': 7,
'h': 8,
'i': 9,
'j': 10,
'k': 11,
'l': 12,
};
final {"e":value} = map;
print(value);
}
模式的种类有很多,而且可以进行适当组合来完成一些稍微复杂点的逻辑,可以看下面关于模式分类的介绍。
重要的一点是:模式不是一种类型,而是一种语法结构,类比语句和表达式,是语法结构的第三种形式
The core of this proposal is a new category of language construct called a pattern. "Expression" and "statement" are both syntactic categories in the grammar. Patterns form a third category.
伴随着模式匹配switch也添加了一些新的特性:
- 在switch中可以使用模式匹配。
- 除了过去的switch语句,增加了switch表达式的写法。
对于这样一个类:
dart
abstract class Shape {
double calculateArea();
}
class Square implements Shape {
final double length;
Square(this.length);
double calculateArea() => length * length;
}
class Circle implements Shape {
final double radius;
Circle(this.radius);
double calculateArea() => math.pi * radius * radius;
}
过去我们会这样写一个辅助方法:
dart
double calculateArea(Shape shape) {
if (shape is Square) {
return shape.length + shape.length;
} else if (shape is Circle) {
return math.pi * shape.radius * shape.radius;
} else {
throw ArgumentError("Unexpected shape.");
}
}
这显得比较繁琐,现在只需要:
dart
double calculateArea(Shape shape) =>
switch (shape) {
Square(length: var l) => l * l,
Circle(radius: var r) => math.pi * r * r
};
而且switch表达式会检测是否匹配完全,如果少写了分支会直接无法通过编译,非常健壮
模式的分类:
模式分类 | 例子 |
---|---|
逻辑或 | `subpattern1 |
逻辑与 | subpattern1 && subpattern2 |
关系 | == expression ,< expression ... |
Cast | foo as String |
Null-check | subpattern? |
Null-assert | subpattern! |
常量 Constant | 123 , null , 'string' math.pi , SomeClass.constant const Thing(1, 2) , const (1 + 2) |
变量 Variable | var bar , String str , final int _ |
标识符 | foo , _ |
括号 | (subpattern) |
List | [subpattern1, subpattern2] |
Map | {"key": subpattern1, someConst: subpattern2} |
Record | (subpattern1, subpattern2) (x: subpattern1, y: subpattern2) |
对象 | SomeClass(x: subpattern1, y: subpattern2) |
- 最开始的匹配的例子里 1就是常量模式,而解构的例子中{"e":value}就是Map-模式(简称Map-Pattern)
在一定的规则下(详细可参阅官方文档)模式之间是可以组合的
- 如:
scss
void test3(){
final (a&&[b,c,d]) = [1,2,3];
print(a);
print([b,c,d]);
}
- 或者:
csharp
void test3(){
dynamic a = [1,2,3];
if (a case (int b || [int _,int b,int _])) {
print(b);
}
}
- 甚至加上guard组成更复杂的逻辑:
csharp
void test1() {
dynamic a = [
[1, 2, 3],
{"name": "Bob", "age": 2},
3
];
if (a case ((Map b || [var _, Map b, ...]) && var c) when c.length <= 3 && b["name"] == "Bob") {
print(b);
} else {
print("no match");
}
}
- 注意上面只是个演示,请在自己清楚自己要做什么的情况下使用,切勿本末倒置
Pattern可以出现的位置:
- 本地变量的声明和赋值(注意一定是local变量,在方法内部声明,不能用在全局、或者对象属性声明)
- 正确示范:
dart
test(int parameter) {
var notFinal;
final unassignedFinal;
late final lateFinal;
if (c) lateFinal = 'maybe assigned';
(notFinal, unassignedFinal, lateFinal) = ('a', 'b', 'c');
}
- 错误示范:
dart
class Person {
static int count = 0;
static final list =[1,2,3];
final [a,b,c] = list; // Wrong
}
- for和for-in loop中
dart
Map<String, int> hist = {
'a': 23,
'b': 100,
};
for (var MapEntry(key: key, value: count) in hist.entries) {
print('$key occurred $count times');
}
- if-case 和 switch-case
dart
void test3() {
final obj = KeyObj(value: "value");
switch (obj) {
case KeyObj(:var value?):
print('ok : $value');
break;
default:
}
if (obj case KeyObj(:var value?)) {
print('ok : $value');
}
}
class KeyObj {
final String? value;
const KeyObj({
this.value,
});
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is KeyObj && other.value == value;
}
@override
int get hashCode => value.hashCode;
}
- 集合字面量的控制流中
dart
void test1() {
final s =[1,2,3];
final list =[1,2,3,if(s case [...,int a,])a+1];
print(list);
}
另外为了避免一些怪异的语法可能导致的歧义,引入Pattern后有一些规定:
- 在变量定义的上下文中,Pattern必须出现在var 或者 final关键词之后
- 用Pattern进行定义的时候必须要初始化
- Pattern出现在定义中不能用逗号分割成多条语句
scss
// Not allowed:
(int, String) (n, s) = (1, "str");
final (a, b) = (1, 2), c = 3, (d, e);
- 在定义上下文中,变量-模式不需要再次声明final或var
java
final r =(1,2);
var (var x, y) = r; // BAD
var (x,y) = r; // GOOD
- 由于Dart存在函数的字面量形式,所以在switch表达式中第一个=>就会被视为swtich中的匹配标识而不是方法中的标识:
dart
void test1() {
num s = 1;
final f = (int i) => i;
final b = switch (s) {
(int a) => (int i) => i,
_ => -1,
};
if (b is Function) {
print(b(10));
}
}
Pattern出现的上下文大致可以分为两类:
1. irrefutable-pattern-context (也可以称为定义&赋值-上下文,主要形容相对于与匹配类型上下文中pattern可以命中也可以不命中,这里的pattern是必须匹配的)
-
只有irrefutable-pattern可以出现在这个位置,不可出现在这里的Pattern有以下几个:
markdown1. logical-or 2. relational 3. null-check 4. constant
-
例如:
dart
void test1() {
final (int? a,int b) =(1,2); // null-check 不能出现在定义上下文中,因为?本身就含有可选与否的含义
dynamic s = (1, 2);
final ((a1, b1) || [a1 b1]) = s; // or-pattern也不能出现
final (String a2||int a2) =s; // 同理 不可出现
final (a3 && b3) = s;// 但是and-pattern是可以出现的
}
2. refutable-pattern-context (也可以称为Matching-上下文)
- 所有pattern都可以出现在这里
目前根据我的总结,可以准确来说根据出现的位置可分为如下两部分,其关键核心区别在于是否在变量声明或赋值:
对于定义&赋值-上下文,是指所有出现定义和赋值的位置,包含:
- 本地变量的声明和赋值
- for和for-in loop中
Matching-上下文可出现的位置包含:
- if-case 和 switch-case
- 集合字面量的控制流中
其他需要注意的点
编译器无法检查到的运行时错误
在匹配上下文中,可能产生运行时错误的只有两种Pattern:
- cast-pattern
dart
void test3() {
num i = 20;
switch (i) {
case var s as double:
print("s");
default:
print("default");
}
}
- Null-assert-pattern
dart
void test4() {
num? i = 30;
i = null;
switch (i) {
case var s!:
print("$s");
default:
}
}
- 其他情况下请放心在Match上下文中使用Pattern,只要编译期没有错误,运行期就不会出错(除非你其他代码出错),这两种Pattern也可以单纯认为只是在匹配之后进行了cast和null-assert,推测是为了保持大家之前对as和 ! 的固有印象,所以直接抛出运行时错误,而不是不匹配本条而进行下一条匹配。
在定义-赋值上下文中可能会产生运行时错误就比较多了,需要非常慎重
除了上面提到的在Match上下文可能出现错误的pattern(cast和null-assert),
- cast-pattern
dart
void test3() {
// 对于Map·
var data = {
'name': 'toly',
'age': 29,
};
if (data case var i as int) { // Wrong
print("match");
}
final a = switch (data) { // Wrong
var i as int => 1,
_=>-1,
};
}
- null-assert-pattern
dart
void test3() {
// 对于Map·
var data = null;
if (data case var i!) { // Wrong
print("match");
}
final a = switch (data) { // Wrong
var i! => 1,
};
}
还有其他几个特别需要注意的点:
- 对于List-Pattern和Recodr-Pattern,数量以及类型(假设指明的话)必须完全匹配
less
final a =[1,2,3];
var [a1,a2,a3,a4] =a; // Wrong
- 对于Map-Pattern,可以不完全列出所有的key,但是列出的key必须要保证等号右边的值一定含有该key
kotlin
void test3() {
// 对于Map·
var data = {
'name': 'toly',
'age': 29,
};
var {'name': name1} = data; // Right
print(name1);
var {'name': name2, "": a2} = data; // Wrong
var {'name': name3, "age1": a3} = data; // Wrong
}
- 当然dynamic类型如果处理不当也会出现运行时错误:
dart
void test3() {
// 对于Map·
dynamic data = {
'name': 'toly',
'age': 29,
};
// final [int i] =data; // Wrong
if (data case int i) { // OK
print("match");
}else{
print("no match");
}
final a = switch (data) {
() => 1,
(int i) => 2,
_ => -1,
}; // Ok
print(a);
}
经过上面的测试和对比,我们可以看出,只要不在Match-上下文中使用as和! 是可以完全相信编译器的,不会出现运行时错误,但在定义-赋值-上下文中需要我们更加谨慎
Null-check Pattern 需要的注意事项
- Null-check Pattern主要是为了匹配非空值,并且将原来的可空变量重新赋值到非空类型的变量上
- 但是要注意Null-check Pattern本身并不会匹配null值:
dart
void test4() {
num? i = 30;
i = null;
switch (i) {
case var s?:
print("$s");
default:
print("default"); //pass
}
}
- 如果希望添加一个能匹配null值的Pattern,需要配合const pattern
dart
void test4() {
num? i = 30;
i = null;
switch (i) {
case var s?:
print("$s");
case null:
print("null"); // pass
default:
print("default");
}
}
Map Pattern的字面量形式至少需要包含一个Entry
Note that mapPatternEntries is not optional, which means it is an error for a map pattern to be empty.
也就是说不准出现下面的代码:
csharp
void test3() {
// 对于Map·
var data = null;
final a = switch (data) {
{} => 1, // Wrong
};
}
如果要匹配任意Map:
javascript
void f(Map<int, String> x) {
if (x case Map()) {} // 切记Map()是任意Map而不是空Map
}
如果只想匹配一个空的Map,:
javascript
void f(Map<int, String> x) {
if (x case Map(isEmpty: true)) {}
}
关于Record含0个或者1个元素
-
如果定义的Record不含元素,则表示为(),注意(),如果包含",",会被警告
ini
var r = (,); //BAD
var r = (); // GOOD
- 但是如果Record只包含一个元素的话,结尾一定要添加",",否则会被视为括号表达式而不是Record
scss
void test3() {
final a = ();
print(a.runtimeType); // ()
final b = (1);
print(b.runtimeType); // int
final c = (1,);
print(c.runtimeType); // (int)
}
// 和上面类似,
void test5() {
final a = (test51());
print(a.runtimeType); // int
final c = (test51(),);
print(c.runtimeType); // (int)
}
int test51() {
return 1;
}
- 同理:对于只包含一个元素的Record的解构同样需要在末尾添加",",只有添加了","号的解构才属于Record模式,否则是括号模式
scss
void test4(){
final source = (1,);
final (a1,) = source;
print(a1.runtimeType); // int
final (a2) = source;
print(a2.runtimeType); // (int)
}
在Match上下文中,一个赤裸的变量会被解释成constant-pattern,而一个带var,final或者Type的变量会被解释成variable-pattern
php
void test8(){
final a =[1,2];
const x =1;
const y =2;
switch (a) {
case x: // 注意这里的x是被解释成常量匹配,x代表的是常量1,那么显然[1,2] != 1
print("x is :$x");
default:
print("default"); // pass
}
}
php
void test8(){
final a =[1,2];
const x =1;
const y =2;
switch (a) {
case var x: // 这里 var x 会被视作variable-pattern,因此会将x绑定为[1,2]
print("x is :$x"); // pass
break;
default:
print("default");
}
}
同理
php
void test8() {
final a = [1, 2];
const x = 1;
const y = 2;
switch (a) {
case [int, int]: // 这里赤裸的int被视作常量对象int,也就是类类型的对象int
print("[int,int]");
default:
print("default"); // pass
}
}
php
void test8() {
final a = [1, 2];
const x = 1;
const y = 2;
switch (a) {
case [int e, int f]: // 这里待类型定义的e,f会被视作variable-pattern,匹配上之后会绑定值
print("[int e,int f] is :[$e,$f]"); // pass
default:
print("default");
}
}
关于int类对象和int常量对象是有区别的,请再看一个例子
php
void test8() {
final a = [int, 2];
switch (a) {
case [int e, int f]:
print("[int e, int f] is :[$e,$f]");
default:
print("default"); // pass
}
}
dart
void test8() {
final a = [int, 2];
switch (a) {
case [Type e, int f]:
print("[Type e, int f] is :[$e,$f]"); // pass
default:
print("default");
}
}
注意上面a中的第一个元素是int类对象,因此只有下面的例子才能匹配
存在一个特例,那就是通配符"_",通配符不论加不加final、var、类型前缀,它都会匹配通过(但是要注意如果加了类型前缀,需要匹配)
php
void test8() {
final a = [int, 2];
switch (a) {
case [_, int f]:
print("[int e, int f] is :[_,$f]"); // pass
default:
print("default");
}
}
php
void test8() {
final a = [int, 2];
switch (a) {
case [final _, int f]:
print("[int _, int f] is :[_,$f]"); // pass
default:
print("default");
}
}
如果加了类型前缀,类型前缀必须匹配通配符才能通过:
php
void test8() {
final a = [int, 2];
switch (a) {
case [int _, int f]:
print("[int _, int f] is :[_,$f]");
default:
print("default"); // pass
}
}
补充:Record类似于List和Map的性质,元素内容其实是变量指向的实际对象,如果修改引用的内容会影响Record本身
ini
void test5() {
var list = [1, 2, 3];
var map = {
"name": "lili",
"age": 18,
};
final r = (list, map);
list = [1];
print(r); // 不影响,因为和变量list本身没有关系 ([1, 2, 3], {name: lili, age: 18})
final (l, m) = r;
l.add(4);
m["name"] = "halo";
print(r); //影响,因为修改了list指向的实际内存 ([1, 2, 3, 4], {name: halo, age: 18})
}