跳到主要内容

Class modifiers for API maintainers(面向 API 维护者的类修饰符)

面向 API 维护者时,类修饰符的核心作用不是“让类看起来更严格”,而是明确用户可以怎样依赖你的公开类型。Dart 3.0 以后,除 abstract 外的类修饰符都可以帮助库作者控制外部库能否构造、继承、实现或混入某个类型。

如果一个类型已经作为公开 API 发布,给它新增 interfacebasefinalsealed 通常会减少用户能力,可能是破坏性变更。设计新 API 时应尽早声明边界;维护旧 API 时要先判断用户是否已经依赖了继承、实现或 with 混入。

先判断 API 使用者需要什么能力

选择修饰符前,先把公开类型的使用方式拆开看:

问题如果答案是“是”常见选择
用户需要直接创建实例吗保留可构造类classbase classinterface classfinal class
用户需要继承你的实现吗允许 extendsclassabstract classbase class
用户只需要替换实现吗允许 implementsabstract interface classinterface class
用户需要把实现作为 Mixin 复用吗允许 withmixinmixin classbase mixin
用户需要穷尽处理所有分支吗固定直接子类型集合sealed class

这里的“用户”指声明该类型的库之外的代码。Dart 的限制以库为边界,而不是以包或目录为边界;同一个库内部仍然可以做更多操作。

迁移 Dart 3.0 前可作为 Mixin 的类

Dart 3.0 之前,满足条件的普通类可以被其他类放在 with 子句中使用。Dart 3.0 之后,类默认不能再作为 Mixin 使用;如果要继续支持这种 API,需要显式声明为 mixin class

timestamps.dart
mixin class Timestamped {
DateTime createdAt = DateTime.now();
}
note.dart
import 'timestamps.dart';

class Note with Timestamped {
Note(this.content);

final String content;
}

void main() {
final note = Note('release notes');
print(note.createdAt);
}

mixin class 同时保留“可以当类继承”和“可以当 Mixin 混入”的能力。若你只是想提供混入能力,不希望用户构造或继承它,使用 mixin 更清晰:

mixin Auditable {
void audit(String action) {
print('audit: $action');
}
}

迁移时可以用两个问题判断:用户是否应该构造这个类型;用户是否应该通过 with 复用它。两个答案都是“是”时使用 mixin class;只需要混入时使用 mixin;不再支持混入时保持普通 class,但这可能破坏旧用户代码。

interface 发布可替换契约

当你希望用户实现同一组能力,但不希望他们继承你的内部实现时,使用 interface。更常见的纯接口形式是 abstract interface class

cache.dart
abstract interface class Cache {
String? read(String key);

void write(String key, String value);
}
memory_cache.dart
import 'cache.dart';

final class MemoryCache implements Cache {
final Map<String, String> _values = {};


String? read(String key) => _values[key];


void write(String key, String value) {
_values[key] = value;
}
}

interface 的好处是 API 维护者可以避免跨库继承带来的脆弱基类问题。用户只能实现接口,不能复用并覆盖你的方法体;因此你的类内部方法调用 this 上的其他方法时,更容易保持可预期行为。

TypeScript 对比:TypeScript 的 interface 主要是静态类型结构;Dart 的 abstract interface class 是 Dart 类型声明,也会参与 implementsis 等类型关系。两者都能表达契约,但 Dart 还通过类修饰符控制外部库能否继承实现。

base 要求用户继承实现

当你的公开类型依赖自身实现或私有成员,并且希望所有子类型都继承这些实现时,使用 base。外部库可以 extends,但不能 implements

connection.dart
base class Connection {
bool _open = false;

void open() {
_open = true;
}

void send(String message) {
if (!_open) {
throw StateError('connection is not open');
}
print(message);
}
}
logging_connection.dart
import 'connection.dart';

final class LoggingConnection extends Connection {

void send(String message) {
print('sending: $message');
super.send(message);
}
}

如果外部库可以 implements Connection,它就不会继承 _openopen() 的状态逻辑,维护者新增依赖私有成员的方法时更容易破坏实现类。base 通过禁止外部 implements,让所有外部子类型都经过真实继承链。

直接继承或实现 base 类型的类必须继续声明为 basefinalsealed。这叫传递性约束,用来避免子类重新打开实现边界。

final 关闭外部子类型

如果用户只应该构造和使用你的类型,不应该通过继承或实现来创建新的子类型,使用 final class

package_version.dart
final class PackageVersion {
const PackageVersion(this.major, this.minor, this.patch);

final int major;
final int minor;
final int patch;


String toString() => '$major.$minor.$patch';
}

final class 对 API 维护者最宽松:你可以更安全地新增实例方法、调整内部实现,用户不会有第三方子类或实现类需要同步适配。它适合值对象、配置对象、结果对象等不希望外部扩展的公开类型。

如果类型只作为静态成员命名空间使用,可以组合成 abstract final class,同时禁止实例化和外部子类型:

abstract final class PackageNames {
static const core = 'core';
static const cli = 'cli';
}

sealed 发布固定分支集合

当一个公开类型代表一组由库作者维护的固定分支,并且你希望用户在 switch 中穷尽处理这些分支时,使用 sealed class

load_state.dart
sealed class LoadState {}

final class Loading extends LoadState {}

final class Loaded extends LoadState {
Loaded(this.data);

final String data;
}

final class Failed extends LoadState {
Failed(this.message);

final String message;
}

String label(LoadState state) {
return switch (state) {
Loading() => 'loading',
Loaded(:final data) => data,
Failed(:final message) => 'error: $message',
};
}

sealed 会让分析器知道所有直接子类型,因此上面的 switch 不需要兜底分支。代价是:以后给 LoadState 新增直接子类型,会让用户已有的穷尽 switch 变成非穷尽,这通常也是破坏性变更。

如果你希望未来可以非破坏性地增加新的子类型,不要用 sealed 固定分支集合;可以考虑 final,让用户不能创建外部子类型,同时仍需要在模式匹配中保留兜底处理。

维护公开 API 的选择顺序

维护库时可以按下面顺序判断:

  1. 先处理 Dart 3.0 的 Mixin 行为变化:旧类如果确实要继续支持 with,显式改成 mixin class
  2. 如果类型是纯契约,优先用 abstract interface class
  3. 如果类型必须继承实现才能正确工作,用 base classabstract base class
  4. 如果类型不应该有外部子类型,用 final class
  5. 如果类型代表固定分支并希望支持穷尽检查,用 sealed class

新增限制前要检查公开 API 的兼容性。对包作者来说,收紧外部继承、实现或混入能力通常应配合主版本升级和迁移说明。