likes
comments
collection
share

Dart 中最新的语法类别 Patterns (模式)

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

Dart 中最新的语法类别 Patterns (2024/4/4)

1. 什么是模式 ?

模式 是 Dart 语言中的一个语法类别,就像语句和表达式一样。模式 表示一组可以与实际值匹配的值的形状。

2. 模式能做什么?

模式可以匹配一个值、解构一个值或者两者兼而有之,这取决于模式的上下文和形状。 首先,模式匹配允许您检查给定值是否:

  • 具有一定的形状。
  • 是一定的常数。
  • 等于其他东西。
  • 具有一定的类型。

然后,模式解构为您提供了一种方便的声明性语法,可将该值分解为其组成部分。同一模式还可以让您在此过程中将变量绑定到部分或全部部分。

3. 匹配

模式总是针对某个值进行测试,以确定该值是否具有您期望的形式。换句话说,您正在检查该值是否与模式匹配。匹配的构成取决于您使用的模式类型。例如,如果值等于模式的常量,则常量模式匹配:

switch (a) {
  case 0:
    print('0');
  case 1:
    print('1');
}

许多模式使用子模式,有时分别称为外部模式和内部模式。模式在其子模式上递归匹配。例如,任何集合类型模式的各个字段可以是变量模式或常量模式:

var a = [1, 2];
const a1 = 1;
const b1 = 2;
switch (a) {
   case [a1, b1]:
     log('$a1 $b1');
   default:
     log('未匹配');
}

注意: 其中 a1 和 b1 必须是常量。因为常量模式的表达式必须是有效的常量。 要忽略匹配值的部分内容,可以使用通配符模式作为占位符。对于列表模式,可以使用rest 元素

  • 通配符

    命名的模式 _ 是一个通配符,可以是变量模式或标识符模式,它不绑定或分配给任何变量。示例:

      var a = [1, 'aa'];
      const a1 = 1;
      const b1 = 2;
      switch (a) {
        case [_, b1]:
          log('$a1 $b1');
        default:
          log('未匹配');
      }
    
  • rest

    列表模式可以包含 rest 剩余元素(...),它允许匹配任意长度的列表。示例如下:

    var a = [1, 'aa', 2, 3, 4];
    const a1 = 1;
    const b1 = 4;
    switch (a) {
      case [a1, ..., b1]:
        log('$a1 $b1');
      default:
        log('未匹配');
    }
    
    或者
    var [c1, ...rest, c2] = a;
    log('$c1, $rest, $c2');
    

4. 解构

当对象和模式匹配时,模式就可以访问对象的数据并分部分提取。换句话说,模式解构了对象:

var a = [1, 2, 3, 4, 5];
var [c1, c2, c3, c4, _] = a;
log('$c1, $c2, $c3, $c4');
// [log] 1, 2, 3, 4

你可以在解构模式中嵌套任何类型的模式。例如,这个 case 模式匹配并解构一个双元素列表,其第一个元素是'a''b'

var a = ['b', 2];
switch (a) {
  case ['a' || 'b', var c]:
    log('$c');
}
// [log] 2
或者
var a = ['a', 2];
switch (a) {
  case ['a' || 'b', var c]:
    log('$c');
}
// [log] 2

5. 模式使用场景

您可以在 Dart 语言的多个地方使用模式:

  • 局部变量声明和赋值
  • for 和 for-in 循环
  • if-case和switch-case
  • 集合文字中的控制流
5-1. 变量声明

Dart 允许局部变量声明的任何地方使用模式变量声明。模式与声明右侧的值匹配。匹配后,它会解构该值并将其绑定到新的局部变量:

// var
var (a1, [a2, a3]) = (1, ['a', 2]);
log('$a1, $a2, $a3');
// [log] 1, a, 2

// final
final (b1, [b2, b3]) = (2, ['b', 3]);
log('$b1, $b2, $b3');
// [log] 2, b, 3

模式变量声明必须以 或 开头varfinal后跟模式。

5-2. 变量赋值

变量赋值模式位于赋值的左侧。首先,它会解构匹配的对象。然后,它会将值分配给现有变量,而不是绑定新变量。 使用变量赋值模式交换两个变量的值,而无需声明第三个临时变量:

var (a1, b1) = (119, 110); // a1 = 119 、 b1 = 110 ,这是变量声明和解构
(b1, a1) = (a1, b1); // b1 = (a1 = 119)  、 a1 = (b1 = 110)
log('$a1,  $b1'); // a1 = 100 ; b1 = 119
5-3. Switch 语句和表达式

每个 case 子句都包含一个模式。这适用于switch 语句和表达式,以及if-case 语句。您可以在 case 中使用任何类型的模式。

const first = 2;
const last = 10;
var a = 11;
switch (a) {
  case 1:
    log('one');
  case >= first && <= last:
    log('in range');
  case (var a, var b):
    log('$a ,  $b');
  default:
}

逻辑或模式对于在 switch 表达式或语句中让多个案例共享一个主体很有用:

var a = 1;
var isPre = switch (a) { 1 || 3 || 6 => true, _ => false };
log('$isPre'); 
// [log] true

Switch 语句可以让多个案例共享一个主体而不使用逻辑或模式,但它们对于允许多个案例共享一个保护仍然非常有用:

// var obj = const Person(11);
var obj = const Monkey(12);
switch (obj) {
    case Person(age: var s) || Monkey(age: var s) when s > 10:
      log('${obj.age}'); // [log] 12
    default:
      log('未匹配');
}

class Person {
  const Person(this.age);
  final int age;
}

class Monkey {
  const Monkey(this.age);
  final int age;
}

注意: 第 4 行, 对象模式只能使用命名字段。

5-4. for 和 for-in 循环

您可以在for 和 for-in 循环中使用模式来迭代和解构集合中的值。 此示例在 for-in 循环中使用对象解构MapEntry来解构调用返回的对象<Map>.entries

final Map<String, int> lilei = {'id': 1890877, 'tel': 110};
for (var MapEntry(key: key, value: value) in lilei.entries) {
  log('$key  $value');
}
// [log] id 1890877
// [log] tel 110

对象模式检查 是否hist.entries具有命名类型MapEntry,然后递归到命名字段子模式keyvalue。它在每次迭代中调用keygetter 和valuegetter ,并将结果分别绑定到局部变量keycount

将 getter 调用的结果绑定到同名变量是一种常见的用例,因此对象模式还可以从变量subpattern推断 getter 名称。这允许您将变量模式从冗余的东西简化key: key:key

final Map<String, int> lilei = {'id': 1890877, 'tel': 110};
for (var MapEntry(: key, : value) in lilei.entries) {
  log('$key  $value');
}
// [log] id 1890877
// [log] tel 110
6. 模式的应用
  • 解构多重回报

    记录允许从单个函数调用中聚合和返回多个值。模式添加了将记录的字段直接解构为局部变量的能力,与函数调用内联。 而不是为每个记录字段单独声明新的局部变量,如下所示:

    var info = getInfo(1);
    var name = info.$1;
    var age = info.$2;
    log('$name - $age');
    // [log] 网二 - 19
    
    (String, int) getInfo(int id) {
      return ('网二', 19);
    }
    

    您可以使用变量声明或赋值模式以及记录模式作为其子模式将函数返回的记录字段解构为局部变量:

    var (name, age) = getInfo(2);
    log('$name - $age');
    // [log] 网二 - 19
    
  • 解构类实例

    对象模式与命名对象类型相匹配,允许您使用对象类已经公开的 getter 来解构其数据。 要解构类的实例,请使用命名类型,后跟括号中的要解构的属性:

    final Person lilei = Person('lilei', 18);
    var Person(:name) = lilei;
    log(name);
    // [log] lilei
    
  • 代数数据类型

    对象解构和 switch case 有利于以代数数据类型风格编写代码。在以下情况下使用此方法:
    
    • 你有一个相关类型的家族。
    • 您的操作需要每种类型的特定行为。
    • 您希望将该行为分组到一个位置,而不是将其分散到所有不同的类型定义中。

    不要将操作实现为每种类型的实例方法,而是将操作的变体保留在切换子类型的单个函数中:

    const Person person = Person('lilei', 19);
    final age = getAge(person);
    log('$age');
    const Monkey monkey = Monkey(19);
    final mAge = getAge(monkey);
    log('$mAge');
    
    sealed class Age {}
    
    class Person implements Age {
      const Person(this.name, this.age);
      final String name;
      final int age;
    }
    
    class Monkey implements Age {
      const Monkey(this.age);
      final int age;
    }
    
    int getAge(Age age) {
      return switch (age) {
        Person(age: var a) => a + 40,
        Monkey(age: var a) => a + 40,
      };
    }
    
  • 验证传入的 JSON

    映射和列表模式非常适合解构 JSON 数据中的键值对:

    var json = {
     'user': ['LiLei', 19],
     'num': [110, 119]
    };
    
    var {'user': [name, age]} = json;
    log('$name - $age');
    // [log] LiLei - 19
    
    var {'num': [tel, qq]} = json;
    log('$tel - $qq');
    // [log] 110 - 119
    

    如果您知道 JSON 数据具有您期望的结构,则前面的示例是现实的。但数据通常来自外部来源,例如通过网络。您需要先对其进行验证以确认其结构。 单个案例作为if-case语句效果最好。模式提供了一种更具声明性且更简洁的验证 JSON 的方法:

    var json = {
      'user': ['LiLei', 19],
      'num': [110, 119]
    };
    
    if (json case {'user': [String name, int age]}) {
      log('$name - $age');
      // [log] LiLei - 19
    }