目录

扩展类型

扩展类型是一种编译时抽象,它使用不同的、仅静态的接口“包装”现有类型。它们是 静态 JS 交互操作 的主要组成部分,因为它们可以轻松修改现有类型的接口(对于任何类型的交互操作都至关重要),而不会产生实际包装器的成本。

扩展类型对可用于底层类型(称为 表示类型 )的对象的操作集(或接口)施加约束。在定义扩展类型的接口时,您可以选择重用表示类型的一些成员,省略其他成员,替换其他成员以及添加新功能。

以下示例包装 int 类型以创建一个扩展类型,该类型仅允许对 ID 号有意义的操作:

dart
extension type IdNumber(int id) {
  // 包装 'int' 类型的 '<' 运算符:
  operator <(IdNumber other) => id < other.id;
  // 例如,不声明 '+' 运算符,
  // 因为对 ID 号进行加法运算没有意义。
}

void main() {
  // 没有扩展类型的约束,
  // 'int' 会将 ID 号暴露给不安全的运算:
  int myUnsafeId = 42424242;
  myUnsafeId = myUnsafeId + 10; // 这可以工作,但不应该允许用于 ID。

  var safeId = IdNumber(42424242);
  safeId + 10; // 编译时错误:没有 '+' 运算符。
  myUnsafeId = safeId; // 编译时错误:类型错误。
  myUnsafeId = safeId as int; // 正确:运行时强制转换为表示类型。
  safeId < IdNumber(42424241); // 正确:使用包装的 '<' 运算符。
}

语法

#

声明

#

使用 extension type 声明和名称定义新的扩展类型,后跟括号中的 表示类型声明

dart
extension type E(int i) {
  // 定义操作集。
}

表示类型声明 (int i) 指定扩展类型 E 的底层类型为 int ,并且对 表示对象 的引用名为 i 。声明还引入:

  • 一个隐式 getter,用于返回表示类型的表示对象: int get i
  • 一个隐式构造函数: E(int i) : i = i

表示对象使扩展类型能够访问底层类型的对象。该对象在扩展类型主体中处于作用域内,您可以使用其名称作为 getter 来访问它:

  • 在扩展类型主体中使用 i (或在构造函数中使用 this.i )。
  • 在外部使用属性提取使用 e.i (其中 e 的静态类型为扩展类型)。

扩展类型声明也可以像类或扩展一样包含 类型参数

dart
extension type E<T>(List<T> elements) {
  // ...
}

构造函数

#

您可以在扩展类型的正文中选择声明 构造函数 。表示声明本身就是一个隐式构造函数,因此默认情况下会代替扩展类型的无名构造函数。任何其他非重定向生成构造函数都必须使用其初始化列表或形式参数中的 this.i 初始化表示对象的实例变量。

dart
extension type E(int i) {
  E.n(this.i);
  E.m(int j, String foo) : i = j + foo.length;
}

void main() {
  E(4); // 隐式无名构造函数。
  E.n(3); // 命名构造函数。
  E.m(5, "Hello!"); // 带有附加参数的命名构造函数。
}

或者,您可以命名表示声明构造函数,在这种情况下,正文中可以容纳一个无名构造函数:

dart
extension type const E._(int it) {
  E(): this._(42);
  E.otherName(this.it);
}

void main2() {
  E();
  const E._(2);
  E.otherName(3);
}

您还可以完全隐藏构造函数,而不仅仅是定义一个新的构造函数,使用与类相同的私有构造函数语法 _ 。例如,如果您只想让客户端使用 String 构造 E ,即使底层类型是 int

dart
extension type E._(int i) {
  E.fromString(String foo) : i = int.parse(foo);
}

您还可以声明转发生成构造函数或 工厂构造函数 (也可以转发到子扩展类型的构造函数)。

成员

#

在扩展类型的正文中声明成员以定义其接口,就像您对类成员所做的那样。扩展类型成员可以是方法、getter、setter 或运算符(不允许使用非 external实例变量抽象成员

dart
extension type NumberE(int value) {
  // 运算符:
  NumberE operator +(NumberE other) =>
      NumberE(value + other.value);
  // Getter:
  NumberE get myNum => this;
  // 方法:
  bool isValid() => !value.isNegative;
}

表示类型的接口成员不是扩展类型的接口成员 默认情况下 。要使表示类型的单个成员在扩展类型上可用,您必须在扩展类型定义中为其编写声明,就像 NumberE 中的 operator + 一样。您还可以定义与表示类型无关的新成员,例如 i getter 和 isValid 方法。

实现

#

您可以选择使用 implements 子句来:

  • 在扩展类型上引入子类型关系,以及
  • 将表示对象的成员添加到扩展类型接口。

implements 子句引入了一种 适用性 关系,就像 扩展方法 与其 on 类型之间的关系一样。适用于超类型的成员也适用于子类型,除非子类型具有相同成员名称的声明。

扩展类型只能实现:

  • 其表示类型 。这使得表示类型的所有成员都隐式可用于扩展类型。

    dart
    extension type NumberI(int i)
      implements int{
      // 'NumberI' 可以调用 'int' 的所有成员,
      // 以及它在此处声明的任何其他内容。
    }
  • 其表示类型的超类型 。这使得超类型的成员可用,但不一定所有表示类型的成员都可用。

    dart
    extension type Sequence<T>(List<T> _) implements Iterable<T> {
      // 比 List 更好的操作。
    }
    
    extension type Id(int _id) implements Object {
      // 使扩展类型不可为空。
      static Id? tryParse(String source) => int.tryParse(source) as Id?;
    }
  • 另一个扩展类型 ,该类型对相同的表示类型有效。这允许您在多个扩展类型中重用操作(类似于多重继承)。

    dart
    extension type const Opt<T>._(({T value})? _) {
      const factory Opt(T value) = Val<T>;
      const factory Opt.none() = Non<T>;
    }
    extension type const Val<T>._(({T value}) _) implements Opt<T> {
      const Val(T value) : this._((value: value));
      T get value => _.value;
    }
    extension type const Non<T>._(Null _) implements Opt<Never> {
      const Non() : this._(null);
    }

阅读 用法 部分以了解 implements 在不同场景中的影响。

@redeclare

#

声明与超类型的成员共享名称的扩展类型成员 不是 像类之间那样的覆盖关系,而是 重新声明 。扩展类型成员声明 完全替换 任何具有相同名称的超类型成员。不可能为同一个函数提供替代实现。

您可以使用 @redeclare 注解来告诉编译器您 有意 选择使用与超类型成员相同的名称。然后,如果实际上并非如此,例如如果其中一个名称输入错误,分析器会警告您。

dart
extension type MyString(String _) implements String {
  // 替换 'String.operator[]'
  @redeclare
  int operator [](int index) => codeUnitAt(index);
}

您还可以启用 lint annotate_redeclares 以获取警告,如果您声明一个隐藏超接口成员且 @redeclare 注解的扩展类型方法。

用法

#

要使用扩展类型,请像使用类一样创建实例:通过调用构造函数:

dart
extension type NumberE(int value) {
  NumberE operator +(NumberE other) =>
      NumberE(value + other.value);

  NumberE get next => NumberE(value + 1);
  bool isValid() => !value.isNegative;
}

void testE() {
  var num = NumberE(1);
}

然后,您可以像使用类对象一样在对象上调用成员。

扩展类型有两种同样有效但本质上不同的核心用例:

  1. 为现有类型提供 扩展的 接口。
  2. 为现有类型提供 不同的 接口。

1. 为现有类型提供 扩展的 接口

#

当扩展类型 实现 其表示类型时,您可以将其视为“透明的”,因为它允许扩展类型“查看”底层类型。

透明的扩展类型可以调用表示类型的所有成员(未 重新声明 的成员),以及它定义的任何辅助成员。这为现有类型创建了一个新的 扩展的 接口。新接口可用于静态类型为扩展类型的表达式。

这意味着您可以 调用 表示类型的成员(不像 非透明的 扩展类型),如下所示:

dart
extension type NumberT(int value)
  implements int {
  // 没有明确声明 'int' 的任何成员。
  NumberT get i => this;
}

void main () {
  // 全部正确:透明性允许在扩展类型上调用 `int` 成员:
  var v1 = NumberT(1); // v1 类型:NumberT
  int v2 = NumberT(2); // v2 类型:int
  var v3 = v1.i - v1;  // v3 类型:int
  var v4 = v2 + v1; // v4 类型:int
  var v5 = 2 + v1; // v5 类型:int
  // 错误:扩展类型接口对表示类型不可用
  v2.i;
}

您还可以拥有一个“大部分透明”的扩展类型,该类型通过重新声明超类型的给定成员名称来添加新成员并调整其他成员。例如,这允许您对方法的某些参数使用更严格的类型,或使用不同的默认值。

另一种大部分透明的扩展类型方法是实现表示类型的超类型。例如,如果表示类型是私有的,但其超类型定义了对客户端重要的接口部分。

2. 为现有类型提供 不同的 接口

#

一个不是 透明的 扩展类型

(不 实现 其表示类型的)扩展类型在静态上被视为一种全新的类型,与其表示类型不同。您不能将其赋值给其表示类型,它也不会公开其表示类型的成员。

例如,考虑我们在 用法 下声明的 NumberE 扩展类型:

dart
void testE() {
  var num1 = NumberE(1);
  int num2 = NumberE(2); // 错误:无法将 'NumberE' 赋值给 'int'。

  num1.isValid(); // 正确:扩展成员调用。
  num1.isNegative(); // 错误:'NumberE' 未定义 'int' 成员 'isNegative'。

  var sum1 = num1 + num1; // 正确:'NumberE' 定义了 '+'。
  var diff1 = num1 - num1; // 错误:'NumberE' 未定义 'int' 成员 '-'。
  var diff2 = num1.value - 2; // 正确:可以使用引用访问表示对象。
  var sum2 = num1 + 2; // 错误:无法将 'int' 赋值给参数类型 'NumberE'。

  List<NumberE> numbers = [
    NumberE(1),
    num1.next, // 正确:'next' getter 返回类型 'NumberE'。
    1, // 错误:无法将 'int' 元素赋值给列表类型 'NumberE'。
  ];
}

您可以通过这种方式使用扩展类型来 替换 现有类型的接口。这允许您模拟对新类型的约束有意义的接口(例如介绍中的 IdNumber 示例),同时还可以受益于简单预定义类型(如 int )的性能和便利性。

此用例与包装类的完全封装最为接近(但在现实中只是一个 某种程度上 受保护的 抽象)。

类型注意事项

#

扩展类型是一种编译时包装结构。在运行时,绝对没有扩展类型的痕迹。任何类型查询或类似的运行时操作都在表示类型上进行。

这使得扩展类型成为一个 不安全 的抽象,因为您总是可以在运行时找到表示类型并访问底层对象。

动态类型测试( e is T )、强制转换( e as T )和其他运行时类型查询(如 switch (e) ...if (e case ...) )都计算为底层表示对象,并针对该对象的运行时类型进行类型检查。当 e 的静态类型是扩展类型时,以及当针对扩展类型进行测试时( case MyExtensionType(): ... ),情况也是如此。

dart
void main() {
  var n = NumberE(1);

  // 'n' 的运行时类型是表示类型 'int'。
  if (n is int) print(n.value); // 打印 1。

  // 可以在运行时对 'n' 使用 'int' 方法。
  if (n case int x) print(x.toRadixString(10)); // 打印 1。
  switch (n) {
    case int(:var isEven): print("$n (${isEven ? "even" : "odd"})"); // 打印 1 (odd)。
  }
}

类似地,在此示例中,匹配值的静态类型是扩展类型的静态类型:

dart
void main() {
  int i = 2;
  if (i is NumberE) print("It is"); // 打印 'It is'。
  if (i case NumberE v) print("value: ${v.value}"); // 打印 'value: 2'。
  switch (i) {
    case NumberE(:var value): print("value: $value"); // 打印 'value: 2'。
  }
}

使用扩展类型时,务必注意此特性。始终记住,扩展类型在编译时存在且很重要,但在编译 期间 会被擦除。

例如,考虑一个静态类型为扩展类型 E 的表达式 e ,而 E 的表示类型为 R 。那么, e 值的运行时类型是 R 的子类型。甚至类型本身也会被擦除; List<E> 在运行时与 List<R> 完全相同。

换句话说,真正的包装类可以封装包装对象,而扩展类型只是对包装对象的编译时视图。虽然真正的包装器更安全,但权衡是扩展类型使您可以选择避免包装器对象,这在某些情况下可以大大提高性能。