跳到主要内容

Extension methods(扩展方法)

扩展方法可以在不修改原类型、也不创建子类的情况下,为现有类型增加可调用的成员。扩展的选择发生在编译期,取决于接收表达式的静态类型,因此它不是对类本身进行运行时修改。

声明扩展

extension StringParsing on String {
int? toIntOrNull() => int.tryParse(this);

bool get isBlank => trim().isEmpty;
}

void main() {
print('42'.toIntOrNull()); // 42
print(' '.isBlank); // true
}

on String 指定扩展适用的静态类型。扩展体中可以使用 this 访问接收值,也可以声明方法、getter、setter 和运算符。

泛型扩展

扩展可以声明类型参数,并保留接收类型的类型信息:

extension IterableExtras<T> on Iterable<T> {
T? get firstOrNull {
final iterator = this.iterator;
return iterator.moveNext() ? iterator.current : null;
}
}

void main() {
print(<int>[1, 2, 3].firstOrNull); // 1
print(<String>[].firstOrNull); // null
}

这里 T 会从 Iterable<T> 的静态类型推断,因此 List<int>.firstOrNull 的结果是 int?

静态分派

扩展成员根据变量的静态类型选择,不会像实例方法那样按运行时类型动态分派:

extension NumberLabel on num {
String label() => 'num: $this';
}

extension IntegerLabel on int {
String label() => 'int: $this';
}

void main() {
int integer = 7;
num number = integer;

print(integer.label()); // int: 7
print(number.label()); // num: 7
}

两个变量指向相同数值,但静态类型不同,所以选择了不同扩展。

如果接收值的静态类型是 dynamic,编译器不会解析扩展成员:

extension StringGreeting on String {
String greet() => 'Hello, $this';
}

void main() {
dynamic name = 'Dart';
// name.greet(); // 运行时 NoSuchMethodError

print((name as String).greet()); // Hello, Dart
}

与实例成员同名

真实的实例成员优先于扩展成员。扩展不能覆盖或替换类已经定义的行为:

extension ListDescription<T> on List<T> {
String toString() => 'extended list';
}

void main() {
print([1, 2].toString()); // [1, 2]
}

调用的是 List 继承到的实例 toString(),扩展中的同名方法不会生效。

解决扩展冲突

如果两个可见扩展都提供同名成员,并且没有一个明显更具体,调用会产生歧义。可以显式写出扩展名称:

extension ShortText on String {
String describe() => 'short: $this';
}

extension DisplayText on String {
String describe() => 'display: $this';
}

void main() {
print(ShortText('Dart').describe());
print(DisplayText('Dart').describe());
}

ShortText('Dart') 不是创建包装对象,而是显式指定本次扩展调用。来自其他库的冲突还可以通过 showhide 或导入前缀控制可见性。

命名与可见性

具名扩展可以被导入库显式引用。以下划线开头的扩展名只在当前库可见;不写名称则是未命名扩展,只能在声明它的库中通过普通成员语法使用。

扩展声明本身不能被实例化,也不会改变接收类型的接口。因此,要求某个类型“必须拥有某成员”时,应使用类、抽象类或接口,而不是依赖扩展方法。

TypeScript 对比:TypeScript 常通过声明合并和修改 prototype 模拟扩展成员,这会影响运行时对象。Dart 扩展方法是静态解析的语法能力,不修改原类或原型,dynamic 调用也不会自动找到它。

常见误区

  • 扩展成员不是实例成员,不能被子类重写,也不参与运行时动态分派。
  • 接收值为 dynamic 时不会应用扩展,必要时先恢复具体静态类型。
  • 实例成员始终优先,扩展不能偷偷替换已有 API。
  • 多个扩展冲突时应显式指定扩展或调整导入,而不是依赖不明确的选择顺序。

小结

  • extension ... on Type 为既有类型增加静态可解析的成员。
  • 扩展可以是泛型,并能声明方法、属性和运算符。
  • 选择哪个扩展取决于静态类型;dynamic 不参与扩展解析。
  • 实例成员优先于扩展成员,冲突时可显式使用扩展名。
  • 扩展方法适合改善调用体验,但不能替代真正的接口契约。