likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 16

模式的作用:

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

  1. 匹配:
switch (number) {
  // Constant pattern matches if 1 == number.
  case 1:
    print('one');
}
  1. 解构:
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也添加了一些新的特性:

  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 || subpattern2
逻辑与subpattern1 && subpattern2
关系== expression ,< expression ...
Castfoo as String
Null-checksubpattern?
Null-assertsubpattern!
常量 Constant123null'string' math.piSomeClass.constant const Thing(1, 2)const (1 + 2)
变量 Variablevar 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-context (也可以称为定义&赋值-上下文,主要形容相对于与匹配类型上下文中pattern可以命中也可以不命中,这里的pattern是必须匹配的)

  • 只有irrefutable-pattern可以出现在这个位置,不可出现在这里的Pattern有以下几个:

      1.  logical-or
      2.  relational
      3.  null-check
      4.  constant
    
  • 例如:

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是可以出现的
}

void test5() {
  final p = Person(firstName: "张", lastName: "向东");
  var Person(fullName: name) = p; // 对象-pattern是可以出现的
  print(name);
}
class Person {
  String firstName;
  String lastName;
  Person({
    required this.firstName,
    required this.lastName,
  });
  String get fullName => firstName + lastName;
}

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