Extension types(扩展类型)
扩展类型为现有的表示类型提供一个新的静态接口,同时通常不创建额外的包装对象。它适合为标识符、句柄或互操作值增加类型约束与专用 API;与扩展方法不同,扩展类型本身可以出现在类型标注中。
版本说明:扩展类型从 Dart 3.3 开始可用。
声明扩展类型
extension type UserId(int value) {
bool get isSystem => value == 0;
String format() => 'user-$value';
}
void main() {
UserId id = UserId(42);
print(id.format()); // user-42
print(id.isSystem); // false
}
int 是表示类型(representation type),value 是表示对象。UserId(42) 在静态类型层面产生 UserId,运行时仍以底层 int 表示。
提供受控接口
扩展类型默认只公开自己声明的成员,不会自动暴露表示类型的全部成员:
extension type Score(int _value) {
factory Score.validated(int value) {
if (value < 0 || value > 100) {
throw RangeError.range(value, 0, 100, 'value');
}
return Score(value);
}
int get value => _value;
bool get passed => _value >= 60;
}
私有表示名称 _value 可以避免库外代码直接通过该字段取得底层值。不过,如果库同时公开了不校验的主构造函数 Score(...),调用方仍能绕过 Score.validated(...);需要强制不变量时,应把可绕过的构造入口也设为私有。
实现表示类型的接口
希望扩展类型保留底层类型的接口时,可以显式 implements:
extension type UserName(String value) implements String {
bool get isValid => value.trim().isNotEmpty;
}
void main() {
UserName name = UserName('Dash');
print(name.length); // 4,来自 String 接口
print(name.isValid); // true,扩展类型新增成员
}
这里 UserName 的表示类型已经提供了 String 接口所需的实现,因此不必逐个重新声明。implements String 同时使 UserName 在静态类型关系上可用作 String。
不要默认添加 implements。只暴露专用 API 可以形成更窄的静态边界,减少误用底层能力。
运算符与泛型
扩展类型可以声明运算符,也可以使用类型参数:
extension type Meters(double value) {
Meters operator +(Meters other) => Meters(value + other.value);
bool operator <(Meters other) => value < other.value;
}
extension type Box<T>(T value) {
R map<R>(R Function(T) transform) => transform(value);
}
void main() {
final distance = Meters(1.5) + Meters(2.0);
print(distance.value); // 3.5
print(Box(3).map((value) => value * 2)); // 6
}
虽然 Meters 与 double 运行时表示相同,静态接口仍能阻止把普通 double 直接传给要求 Meters 的 API。
运行时表示与类型检查
扩展类型会在编译过程中被擦除,不具备独立的运行时对象身份:
extension type UserId(int value) {}
void main() {
final id = UserId(42);
print(id.runtimeType); // int
}
因此,不要依赖 runtimeType 或运行时类型测试来区分 UserId 与普通 int。扩展类型提供的主要保证发生在静态分析阶段;值一旦以表示类型流出,其他代码就可能绕过扩展接口。
与扩展方法、包装类的区别
| 能力 | 扩展方法 | 扩展类型 | 包装类 |
|---|---|---|---|
| 可用于类型标注 | 否 | 是 | 是 |
| 默认产生额外包装对象 | 否 | 否 | 是 |
| 能隐藏底层接口 | 否 | 是 | 是 |
| 有独立运行时身份 | 否 | 否 | 是 |
| 适合完整封装与身份语义 | 否 | 有限 | 是 |
扩展类型强调零成本静态接口。如果需要无法绕过的不变量、独立对象身份、运行时类型区分或可变状态,应使用普通类。
TypeScript 对比:TypeScript 常用交叉类型与品牌字段模拟 nominal ID,例如
number & {__brand: 'UserId'}。两者都主要提供静态区分,但 Dart 扩展类型有正式的构造和成员声明语法,还能明确控制是否实现表示类型接口。
常见误区
- 扩展类型不是普通包装类,运行时通常只剩表示值。
- 表示类型的成员不会自动全部出现在扩展接口中,除非显式声明或实现相应接口。
- 公开底层值或未校验构造入口会削弱封装,不能把扩展类型当作绝对安全边界。
- 需要运行时身份和强封装时,应选择普通类。
小结
- 扩展类型在表示类型之上建立新的静态接口,通常没有包装分配成本。
- 表示对象保存底层值,扩展体声明允许调用的专用成员。
- 通过
implements可以选择性保留表示类型的接口。 - 扩展类型支持泛型、运算符和额外构造入口。
- 它的边界主要由静态分析保障,不提供独立运行时身份。