概览
扩展类型是一种编译时抽象,它通过一个不同的、仅静态的接口来"包装"现有类型 。它们是静态 JS 互操作的主要组成部分,因为它们可以轻松修改现有类型的接口(对任何类型的互操作都至关重要),而无需承担实际包装器的开销。
扩展类型对表示类型(即底层类型)的对象可用的一组操作(或接口)施加了约束。在定义扩展类型的接口时,您可以选择重用表示类型的某些成员、忽略其他成员、替换其他成员,并添加新功能。
以下示例包装了 int 类型,创建了一个仅允许对 ID 号码有意义的操作的扩展类型:
dart
extension type IdNumber(int id) {
// Wraps the 'int' type's '<' operator:
operator <(IdNumber other) => id < other.id;
// Doesn't declare the '+' operator, for example,
// because addition does not make sense for ID numbers.
}
void main() {
// Without the discipline of an extension type,
// 'int' exposes ID numbers to unsafe operations:
int myUnsafeId = 42424242;
myUnsafeId = myUnsafeId + 10; // This works, but shouldn't be allowed for IDs.
var safeId = IdNumber(42424242);
safeId + 10; // Compile-time error: No '+' operator.
myUnsafeId = safeId; // Compile-time error: Wrong type.
myUnsafeId = safeId as int; // OK: Run-time cast to representation type.
safeId < IdNumber(42424241); // OK: Uses wrapped '<' operator.
}
扩展类型与包装类具有相同的目的,但不需要创建额外的运行时对象。当需要包装大量对象时,包装类可能会带来高昂开销。由于扩展类型仅是静态的,并在运行时被编译消除,因此它们基本上是零成本的。
扩展方法(也简称为"扩展")是一种与扩展类型类似的静态抽象。然而,扩展方法直接将功能添加到其底层类型的每个实例上。扩展类型则不同;扩展类型的接口仅适用于静态类型为该扩展类型的表达式。默认情况下,它们与其底层类型的接口是截然不同的。
语法
声明
使用 extension type和一个名称来定义新的扩展类型,然后在括号内加上表示类型声明(记住这个词后面都会用到):
dart
extension type Name(int i) {
// Define set of operations.
}
表示类型声明 (int i) 指定了扩展类型 Name 的底层类型是 int,并且表示对象的引用名为 i。
该声明还引入了:
一个以表示类型(底层类型)作为返回类型的**表示对象(都是一个意思,这里指i)**的隐式 getter:int get i。
一个隐式构造函数:Name(int i) : i = i。
表示 getter 提供了对类型为底层类型的表示对象的访问。该 getter 在扩展类型体内有效,您可以像使用任何其他 getter 一样通过其名称访问它:
在扩展类型体内使用 i(或 this.i)。
在外部,对具有(该扩展类型作为静态类型)的表达式 name 使用属性提取:name.i。
扩展类型声明也可以像类或扩展一样包含泛型参数:
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); // Implicit unnamed constructor.
E.n(3); // Named constructor.
E.m(5, "Hello!"); // Named constructor with additional parameters.
}
或者,您也可以为表示声明构造函数命名。在这种情况下,扩展类型体中就可以容纳一个未命名的构造函数:
(这句话真的好绕,建议多读两次, 因为扩展类型必须有表示对象,所以一般都是需要从外部传,如果你想定义一个默认的)
下面的写法不被允许
dart
extension type E(int i) {
E(): this(42);//The unnamed constructor is already defined.
E.otherName(this.i);
}
下面的写法是正确的 (const 正确理解哈,不是固定的一部分,可以没有)
dart
extension type const Name.a(int it) {
Name(): this.a(42);
Name.otherName(this.it);
}
void main2() {
Name();
Name.a(2);
Name.otherName(3);
}
您也可以完全隐藏构造函数,而不仅仅是定义一个新的构造函数,这与类的私有构造函数语法 _ 用法相同。例如,即使底层类型是 int,如果您只希望客户端使用 String 来构造 E:
dart
extension type E._(int i) {
E.fromString(String foo) : i = int.parse(foo);
}
您也可以声明转发生成构造函数或工厂构造函数(后者同样可以转发到子扩展类型的构造函数)。
成员
不允许使用外部实例变量和抽象成员
dart
extension type NumberE(int value) {
// Operator:
NumberE operator +(NumberE other) =>
NumberE(value + other.value);
// Getter:
NumberE get myNum => this;
// Method:
bool isValid() => !value.isNegative;
}
默认情况下,表示类型的接口成员不会成为扩展类型的接口成员。
如果要将表示类型的某个成员暴露给扩展类型使用,您必须在扩展类型的定义中为其编写声明,例如 NumberE 中的 + 运算符。

您还可以定义与表示类型无关的全新成员,例如 i getter 和 isValid 方法。
实现implements
您可以选择使用 implements 子句来:
建立扩展类型之间的子类型关系,并且
将表示对象的成员添加到扩展类型的接口中。
implements 子句引入了一种适用性关系,类似于扩展方法与其 on 类型之间的关系。适用于超类型的成员同样适用于子类型,除非子类型有同名的成员声明。
扩展类型只能实现:
他的表示类型,这使得表示类型的所有成员隐式地对该扩展类型可用。
dart
extension type NumberI(int i)
implements int{
// 'NumberI' 可以调用 'int' 的所有成员,
// 以及它在此声明的任何其他成员。
}
表示类型的超类型。这使得超类型的成员可用,但未必包括表示类型的所有成员。
dart
extension type Sequence<T>(List<T> _) implements Iterable<T> {
// Better operations than List.
}
extension type Id(int _id) implements Object {
// Makes the extension type non-nullable.
static Id? tryParse(String source) => int.tryParse(source) as Id?;
}
@redeclare
在扩展类型中声明与超类型成员同名的成员,并不会形成类似类之间的"重写"关系,而是一种重新声明。扩展类型的成员声明会完全替换任何同名的超类型成员。你无法为同一个函数提供替代实现。
你可以使用 package:meta 中的 @redeclare 注解来告知编译器,你是有意选择使用与超类型成员相同的名称。如果实际情况并非如此(例如,其中一个名称拼写错误),分析器就会发出警告。
dart
import 'package:meta/meta.dart';
extension type MyString(String _) implements String {
// Replaces 'String.operator[]'.
@redeclare
int operator [](int index) => codeUnitAt(index);
}
您还可以启用 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);
}
随后,您可以像操作类对象一样调用该对象上的成员。
扩展类型有两个同样有效但本质上不同的核心用例:
为现有类型提供扩展的接口。
为现有类型提供不同的接口。
无论如何,扩展类型的表示类型永远不会成为其子类型。因此,在需要扩展类型的地方,不能互换使用表示类型。
为现有类型提供扩展的接口 {#provide-extended-interface}
当扩展类型实现其表示类型时,您可以将其视为"透明的",因为它允许扩展类型"看到"底层类型。
透明扩展类型可以调用表示类型的所有成员(未被重新声明的),以及它定义的任何辅助成员。这为现有类型创建了一个新的、扩展的接口。这个新接口对静态类型为该扩展类型的表达式可用。
dart
extension type NumberT(int value)
implements int {
// Doesn't explicitly declare any members of 'int'.
NumberT get i => this;
}
void main () {
// All OK: Transparency allows invoking `int` members on the extension type:
var v1 = NumberT(1); // v1 type: NumberT
int v2 = NumberT(2); // v2 type: int
var v3 = v1.i - v1; // v3 type: int
var v4 = v2 + v1; // v4 type: int
var v5 = 2 + v1; // v5 type: int
// Error: Extension type interface is not available to representation type
v2.i;
}
您也可以拥有一个"基本透明"的扩展类型,它既能添加新成员,又能通过重新声明超类型中的特定成员来调整其他成员。例如,这允许您在方法的某些参数上使用更严格的类型,或不同的默认值。
为现有类型提供不同的接口
非透明的扩展类型(即未实现其表示类型的扩展类型)在静态上被视为一个与其表示类型完全不同的全新类型。您不能将其赋值给其表示类型,它也不会暴露其表示类型的成员。
dart
void testE() {
var num1 = NumberE(1);
int num2 = NumberE(2); // Error: Can't assign 'NumberE' to 'int'.
num1.isValid(); // OK: Extension member invocation.
num1.isNegative(); // Error: 'NumberE' does not define 'int' member 'isNegative'.
var sum1 = num1 + num1; // OK: 'NumberE' defines '+'.
var diff1 = num1 - num1; // Error: 'NumberE' does not define 'int' member '-'.
var diff2 = num1.value - 2; // OK: Can access representation object with reference.
var sum2 = num1 + 2; // Error: Can't assign 'int' to parameter type 'NumberE'.
List<NumberE> numbers = [
NumberE(1),
num1.next, // OK: 'next' getter returns type 'NumberE'.
1, // Error: Can't assign 'int' element to list type 'NumberE'.
];
}
您可以通过这种方式使用扩展类型来替换现有类型的接口。这允许您为新类型的约束(如引言中的 IdNumber 示例)建模一个合适的接口,同时还能受益于简单预定义类型(如 int)的性能和便利性。
这种用例最接近于实现包装类的完全封装(但实际上只是一种有一定保护性的抽象)
类型考量
扩展类型是一种编译时的包装构造。在运行时,扩展类型没有任何痕迹。任何类型查询或类似的运行时操作都作用于表示类型。
这使得扩展类型成为一种不安全的抽象,因为你总是可以在运行时查明表示类型并访问底层对象。
动态类型测试(e is T)、类型转换(e as T)以及其他运行时类型查询(例如 switch (e) ... 或 if (e case ...))都会作用于底层表示对象,并针对该对象的运行时类型进行检查。当 e 的静态类型是扩展类型,或者要测试的类型是扩展类型时(case MyExtensionType(): ...),情况都是如此。
dart
void main() {
var n = NumberE(1);
// The run-time type of 'n' is the representation type 'int'.
if (n is int) print(n); // Prints 1.
// Can use 'int' methods on 'n' at run time.
if (n case int x) print(x.toRadixString(10)); // Prints 1.
switch (n) {
case int(:var isEven): print("$n (${isEven ? "even" : "odd"})"); // Prints 1 (odd).
}
}
类似地,在下面示例中,被匹配值的静态类型是扩展类型:
dart
void main() {
int i = 2;
if (i is NumberE) print("It is"); // Prints 'It is'.
if (i case NumberE v) print("value: ${v.value}"); // Prints 'value: 2'.
switch (i) {
case NumberE(:var value): print("value: $value"); // Prints 'value: 2'.
}
}
当 i 基于类型转换或模式匹配获得静态类型 NumberE 时,i 仍然引用同一个对象,v 也引用与 i 相同的对象,对象本身并未改变。改变的仅仅是静态类型(以及因此我们能在此对象上调用的方法)。
特别要注意的是,类型的改变并不会涉及构造函数的调用 。
如果你希望执行构造函数(例如,为了执行某种验证),则必须显式地调用构造函数(如 NumberE(i))。
在使用扩展类型时,了解这种行为非常重要。始终要记住:扩展类型存在于编译时且仅在编译时有意义,它会在编译过程中被擦除。
例如,假设表达式 e 的静态类型是扩展类型 E。又设 E 的表示类型是 R。那么 e 值的运行时类型就是 R 的子类型。甚至连类型本身也会被擦除;在运行时,List 与 List 完全等同。
换句话说,真正的包装类可以封装被包装的对象,而扩展类型只是被包装对象在编译时的一个视图 。
虽然真正的包装更安全,但扩展类型的优势在于让你可以选择避免包装对象,这在某些场景下能显著提升性能。