扩展类型
扩展类型是一种编译时抽象,它用不同的、仅静态的接口“包装”现有类型。它们是 静态 JS 互操作 的主要组成部分,因为它们可以轻松修改现有类型的接口(对于任何类型的互操作都至关重要),而无需承担实际包装器的成本。
扩展类型对底层类型(称为表示类型)的对象可用的操作集(或接口)实施约束。在定义扩展类型的接口时,您可以选择复用表示类型的某些成员、省略其他成员、替换其他成员以及添加新功能。
以下示例包装了 int
类型,创建了一个扩展类型,该类型只允许对 ID 号码有意义的操作
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
声明和一个名称来定义一个新的扩展类型,后跟括号中的表示类型声明
extension type E(int i) {
// Define set of operations.
}
表示类型声明 (int i)
指定了扩展类型 E
的底层类型是 int
,并且对表示对象的引用名为 i
。该声明还引入了
- 一个用于表示对象的隐式 getter,返回类型为表示类型:
int get i
。 - 一个隐式构造函数:
E(int i) : i = i
。
表示对象使扩展类型能够访问底层类型的对象。该对象在扩展类型主体中处于作用域内,您可以使用其名称作为 getter 来访问它
- 在扩展类型主体内使用
i
(或在构造函数中使用this.i
)。 - 在外部使用属性提取
e.i
(其中e
的静态类型是扩展类型)。
扩展类型声明也可以像类或扩展一样包含 类型参数
extension type E<T>(List<T> elements) {
// ...
}
构造函数
#您可以选择在扩展类型的主体中声明构造函数。表示声明本身是一个隐式构造函数,因此默认情况下它会替代扩展类型的无名构造函数。任何额外的非重定向生成构造函数都必须在其初始化列表或形式参数中使用 this.i
初始化表示对象的实例变量。
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.
}
或者,您可以命名表示声明构造函数,在这种情况下,主体中可以有一个无名构造函数
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
extension type E._(int i) {
E.fromString(String foo) : i = int.parse(foo);
}
您还可以声明转发生成构造函数,或 工厂构造函数(它们也可以转发到子扩展类型的构造函数)。
成员
#在扩展类型的主体中声明成员来定义其接口,方式与类成员相同。扩展类型成员可以是方法、getter、setter 或运算符(不允许非external
实例变量 和 抽象成员)
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
中的 operator +
一样。您还可以定义与表示类型无关的新成员,例如 i
getter 和 isValid
方法。
实现
#您可以选择使用 implements
子句来
- 在扩展类型上引入子类型关系,并且
- 将表示对象的成员添加到扩展类型接口。
implements
子句引入了一种适用性关系,类似于扩展方法与其 on
类型之间的关系。适用于超类型的成员也适用于子类型,除非子类型具有同名成员的声明。
扩展类型只能实现
其表示类型。这使得表示类型的所有成员隐式地对扩展类型可用。
dartextension type NumberI(int i) implements int{ // 'NumberI' can invoke all members of 'int', // plus anything else it declares here. }
其表示类型的超类型。这使得超类型的成员可用,但并非所有表示类型的成员都可用。
dartextension 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?; }
另一个扩展类型,该类型在相同的表示类型上有效。这允许您在多个扩展类型之间重用操作(类似于多重继承)。
dartextension 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
#声明一个与超类型成员同名的扩展类型成员不是像类之间那样的覆盖关系,而是重新声明。扩展类型成员声明完全替换任何同名的超类型成员。不可能为同一函数提供替代实现。
您可以使用 package:meta
中的 @redeclare
注解来告诉编译器您有意选择使用与超类型成员相同的名称。如果实际上并非如此,例如其中一个名称拼写错误,分析器将发出警告。
import 'package:meta/meta.dart';
extension type MyString(String _) implements String {
// Replaces 'String.operator[]'.
@redeclare
int operator [](int index) => codeUnitAt(index);
}
您还可以启用 lint annotate_redeclares
,如果您声明的扩展类型方法隐藏了超接口成员且未使用 @redeclare
进行注解,则会收到警告。
用法
#要使用扩展类型,请像使用类一样创建实例:通过调用构造函数
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. 为现有类型提供扩展接口
#当扩展类型实现其表示类型时,您可以将其视为“透明”,因为它允许扩展类型“看到”底层类型。
透明扩展类型可以调用表示类型的所有成员(未重新声明的),以及它定义的任何辅助成员。这为现有类型创建了一个新的、扩展的接口。新接口可用于静态类型为该扩展类型的表达式。
这意味着您可以调用表示类型的成员(与非透明扩展类型不同),如下所示
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;
}
您还可以拥有一个“部分透明”的扩展类型,它通过重新声明超类型中的给定成员名称来添加新成员和调整其他成员。例如,这可以允许您对方法的某些参数使用更严格的类型,或使用不同的默认值。
另一种部分透明的扩展类型方法是实现表示类型的超类型。例如,如果表示类型是私有的,但其超类型定义了对客户端重要的接口部分。
2. 为现有类型提供不同接口
#一个非透明的扩展类型(即不implement
其表示类型的扩展类型)在静态上被视为一个全新的类型,与它的表示类型不同。您不能将其赋值给它的表示类型,它也不会暴露其表示类型的成员。
例如,我们以在用法下声明的 NumberE
扩展类型为例
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(): ...
),情况都是如此。
void main() {
var n = NumberE(1);
// Run-time type of 'n' is representation type 'int'.
if (n is int) print(n.value); // 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).
}
}
类似地,在此示例中,匹配值的静态类型就是扩展类型的静态类型
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'.
}
}
在使用扩展类型时,了解这一特性非常重要。请始终记住,扩展类型在编译时存在并发挥作用,但在编译期间会被擦除。
例如,考虑一个静态类型为扩展类型 E
的表达式 e
,并且 E
的表示类型为 R
。那么,e
的值的运行时类型是 R
的子类型。即使类型本身也被擦除;List<E>
在运行时与 List<R>
完全相同。
换句话说,一个真正的包装类可以封装一个被包装的对象,而扩展类型只是对被包装对象的编译时视图。虽然真正的包装器更安全,但权衡之下,扩展类型提供了避免包装对象的选项,这在某些情况下可以显著提高性能。