Jansiel Notes

Flutter模式匹配指南:一文精讲Dart3.0你所不知道的Pattern细节和注意事项

模式的作用:

模式是用来进行匹配和解构的;

  1. 匹配:
1switch (number) {
2  // Constant pattern matches if 1 == number.
3  case 1:
4    print(\'one\');
5}
6
  1. 解构:
 1void test1() {
 2  var map = {
 3    \'a\': 1,
 4    \'b\': 2,
 5    \'c\': 3,
 6    \'d\': 4,
 7    \'e\': 5,
 8    \'f\': 6,
 9    \'g\': 7,
10    \'h\': 8,
11    \'i\': 9,
12    \'j\': 10,
13    \'k\': 11,
14    \'l\': 12,
15  };
16  final {\"e\":value} = map;
17  print(value);
18}
19

模式的种类有很多,而且可以进行适当组合来完成一些稍微复杂点的逻辑,可以看下面关于模式分类的介绍。

重要的一点是:模式不是一种类型,而是一种语法结构,类比语句和表达式,是语法结构的第三种形式

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也添加了一些新的特性:

  1. 在switch中可以使用模式匹配。
  2. 除了过去的switch语句,增加了switch表达式的写法。

对于这样一个类:

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;
}

过去我们会这样写一个辅助方法:

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.\");
  }
}

这显得比较繁琐,现在只需要:

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 123null\'string\' math.piSomeClass.constant const Thing(1, 2)const (1 + 2)
变量 Variable var barString strfinal 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)

在一定的规则下(详细可参阅官方文档)模式之间是可以组合的

  • 如:
void test3(){
  final (a&&[b,c,d]) = [1,2,3];
  print(a);
  print([b,c,d]);
}

  • 或者:
void test3(){
  dynamic a = [1,2,3];
  if (a case (int b || [int _,int b,int _])) {
    print(b);
  }
}

  • 甚至加上guard组成更复杂的逻辑:

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可以出现的位置:

  1. 本地变量的声明和赋值(注意一定是local变量,在方法内部声明,不能用在全局、或者对象属性声明)
  • 正确示范:
test(int parameter) {
  var notFinal;
  final unassignedFinal;
  late final lateFinal;

  if (c) lateFinal = \'maybe assigned\';

  (notFinal, unassignedFinal, lateFinal) = (\'a\', \'b\', \'c\');
}

  • 错误示范:
class Person {
  static int count = 0;
  static final list =[1,2,3];
  final [a,b,c] = list;  // Wrong
}

  1. for和for-in loop中
Map<String, int> hist = {
  \'a\': 23,
  \'b\': 100,
};

for (var MapEntry(key: key, value: count) in hist.entries) {
  print(\'$key occurred $count times\');
}

  1. if-case 和 switch-case

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;
}

  1. 集合字面量的控制流中
void test1() {
  final s =[1,2,3];
  final list =[1,2,3,if(s case [...,int a,])a+1];
  print(list);
}

另外为了避免一些怪异的语法可能导致的歧义,引入Pattern后有一些规定:

  1. 在变量定义的上下文中,Pattern必须出现在var 或者 final关键词之后
  2. 用Pattern进行定义的时候必须要初始化
  3. Pattern出现在定义中不能用逗号分割成多条语句
// Not allowed:
(int, String) (n, s) = (1, \"str\");
final (a, b) = (1, 2), c = 3, (d, e);

  1. 在定义上下文中,变量-模式不需要再次声明final或var
final r =(1,2);
var (var x, y) = r; // BAD
var (x,y) = r; // GOOD

  1. 由于Dart存在函数的字面量形式,所以在switch表达式中第一个=>就会被视为swtich中的匹配标识而不是方法中的标识:
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](https://link.juejin.cn?target=https%3A%2F%2Fdart.dev%2Fresources%2Fglossary%23irrefutable-pattern "https://dart.dev/resources/glossary#irrefutable-pattern")-context (也可以称为定义&赋值-上下文,主要形容相对于与匹配类型上下文中pattern可以命中也可以不命中,这里的pattern是必须匹配的)
  • 只有irrefutable-pattern可以出现在这个位置,不可出现在这里的Pattern有以下几个:

    11.  logical-or
    22.  relational
    33.  null-check
    44.  constant
    5
    

- 例如:

```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](https://link.juejin.cn?target=https%3A%2F%2Fdart.dev%2Fresources%2Fglossary%23refutable-pattern "https://dart.dev/resources/glossary#refutable-pattern")-context (也可以称为Matching-上下文)
  • 所有pattern都可以出现在这里

目前根据我的总结,可以准确来说根据出现的位置可分为如下两部分,其关键核心区别在于是否在变量声明或赋值:

对于定义&赋值-上下文,是指所有出现定义和赋值的位置,包含:
  1. 本地变量的声明和赋值
  2. for和for-in loop中
Matching-上下文可出现的位置包含:
  1. if-case 和 switch-case
  2. 集合字面量的控制流中

其他需要注意的点

编译器无法检查到的运行时错误

在匹配上下文中,可能产生运行时错误的只有两种Pattern:
  1. cast-pattern
void test3() {
  num i = 20;
  switch (i) {
    case var s as double:
      print(\"s\");
    default:
      print(\"default\");
  }
}

  1. Null-assert-pattern
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
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
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,数量以及类型(假设指明的话)必须完全匹配
final a =[1,2,3];
var [a1,a2,a3,a4] =a; // Wrong

  • 对于Map-Pattern,可以不完全列出所有的key,但是列出的key必须要保证等号右边的值一定含有该key
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类型如果处理不当也会出现运行时错误:
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值:
void test4() {
  num? i = 30;
  i = null;
  switch (i) {
    case var s?:
      print(\"$s\");
    default:
      print(\"default\"); //pass
  }
}

  • 如果希望添加一个能匹配null值的Pattern,需要配合const pattern
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.

######## 也就是说不准出现下面的代码:

void test3() {
    // 对于Map·
  var data = null;
  final a = switch (data) {
    {}  => 1, // Wrong
  };

}

######## 如果要匹配任意Map:

void f(Map<int, String> x) {

  if (x case Map()) {} // 切记Map()是任意Map而不是空Map

}

######## 如果只想匹配一个空的Map,:


void f(Map<int, String> x) {
  if (x case Map(isEmpty: true)) {}
}

关于Record含0个或者1个元素

  1. 如果定义的Record不含元素,则表示为(),注意(),如果包含",",会被警告
var r = (,); //BAD
var r = (); // GOOD

  1. 但是如果Record只包含一个元素的话,结尾一定要添加",",否则会被视为括号表达式而不是Record
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;
}

  1. 同理:对于只包含一个元素的Record的解构同样需要在末尾添加",",只有添加了","号的解构才属于Record模式,否则是括号模式
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

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
  }
}

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\");
  }
}

同理
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
  }
}

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常量对象是有区别的,请再看一个例子
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
  }
}

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、类型前缀,它都会匹配通过(但是要注意如果加了类型前缀,需要匹配)
void test8() {
  final a = [int, 2];
  switch (a) {
    case [_, int f]:
      print(\"[int e, int f] is :[_,$f]\"); // pass
    default:
      print(\"default\");
  }
}

void test8() {
  final a = [int, 2];
  switch (a) {
    case [final _, int f]:
      print(\"[int _, int f] is :[_,$f]\"); // pass
    default:
      print(\"default\");
  }
}

如果加了类型前缀,类型前缀必须匹配通配符才能通过:

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本身

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})
}