扩展类型
扩展类型是一种编译时抽象,它使用不同的、仅限静态的接口“包装”现有类型。它们是静态 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
#声明一个与超类型成员共享名称的扩展类型成员不是像类之间那样的重写关系,而是一个重新声明。扩展类型成员声明完全替换任何具有相同名称的超类型成员。不可能为同一个函数提供替代实现。
您可以使用 @redeclare
注释来告诉编译器您有意选择使用与超类型成员相同的名称。如果这实际上不是真的,例如,如果其中一个名称键入错误,则分析器会警告您。
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>
完全相同。
换句话说,真正的包装器类可以封装一个被包装的对象,而扩展类型只是对被包装对象的一种编译时视图。 虽然真正的包装器更安全,但权衡的是扩展类型为您提供了避免包装器对象的选项,这可以在某些情况下大大提高性能。
除非另有说明,本网站上的文档反映了 Dart 3.6.0。页面上次更新时间为 2024-10-22。查看源代码 或报告问题。