扩展类型
扩展类型是一种编译时抽象,它用不同的、仅静态的接口“包装”现有类型。它们是 静态 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. 为现有类型提供**不同**的接口
#一个不是透明的扩展类型(没有实现
其表示类型)在静态上被视为一个全新的类型,与它的表示类型不同。不能将其分配给它的表示类型,并且它不公开它的表示类型的成员。
例如,考虑在用法下声明的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.5.3。页面最后更新于 2024-10-22。 查看源代码 或 报告问题.