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

模式的作用:

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

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

  1. 在switch中可以使用模式匹配。
  2. 除了过去的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可以出现的位置:

  1. 本地变量的声明和赋值(注意一定是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
}
  1. 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');
}
  1. 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;
}
  1. 集合字面量的控制流中
dart 复制代码
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出现在定义中不能用逗号分割成多条语句
scss 复制代码
// Not allowed:
(int, String) (n, s) = (1, "str");
final (a, b) = (1, 2), c = 3, (d, e);
  1. 在定义上下文中,变量-模式不需要再次声明final或var
java 复制代码
final r =(1,2);
var (var x, y) = r; // BAD
var (x,y) = r; // GOOD
  1. 由于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有以下几个:

    markdown 复制代码
      1.  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都可以出现在这里

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

对于定义&赋值-上下文,是指所有出现定义和赋值的位置,包含:

  1. 本地变量的声明和赋值
  2. for和for-in loop中

Matching-上下文可出现的位置包含:

  1. if-case 和 switch-case
  2. 集合字面量的控制流中

其他需要注意的点

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

在匹配上下文中,可能产生运行时错误的只有两种Pattern:

  1. cast-pattern
dart 复制代码
void test3() {
  num i = 20;
  switch (i) {
    case var s as double:
      print("s");
    default:
      print("default");
  }
}
  1. 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个元素

  1. 如果定义的Record不含元素,则表示为(),注意(),如果包含",",会被警告

ini 复制代码
var r = (,); //BAD
var r = (); // GOOD
  1. 但是如果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;
}
  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})
}
相关推荐
孤鸿玉3 小时前
Fluter InteractiveViewer 与ScrollView滑动冲突问题解决
flutter
叽哥10 小时前
Flutter Riverpod上手指南
android·flutter·ios
BG1 天前
Flutter 简仿Excel表格组件介绍
flutter
zhangmeng1 天前
FlutterBoost在iOS26真机运行崩溃问题
flutter·app·swift
恋猫de小郭1 天前
对于普通程序员来说 AI 是什么?AI 究竟用的是什么?
前端·flutter·ai编程
卡尔特斯1 天前
Flutter A GlobalKey was used multipletimes inside one widget'schild list.The ...
flutter
w_y_fan1 天前
Flutter 滚动组件总结
前端·flutter
醉过才知酒浓1 天前
Flutter Getx 的页面传参
flutter
火柴就是我2 天前
flutter 之真手势冲突处理
android·flutter
Speed1232 天前
`mockito` 的核心“打桩”规则
flutter·dart