Overview & usage(类修饰符概览与用法)
类修饰符(class modifier)用于控制一个类型能否被实例化,以及其他库能否继承或实现它。Dart 3.0 引入了完整的类修饰符体系,让 API 作者可以明确表达“只提供接口”“必须继承实现”或“禁止外部创建子类型”等约束。
最常见的纯接口写法是 abstract interface class:
abstract interface class DataStore {
String? read(String key);
void write(String key, String value);
}
final class MemoryDataStore implements DataStore {
final Map<String, String> _values = {};
String? read(String key) => _values[key];
void write(String key, String value) {
_values[key] = value;
}
}
void main() {
final store = MemoryDataStore();
store.write('language', 'Dart');
print(store.read('language')); // Dart
}
abstract 表示不能直接创建 DataStore 实例,interface 表示其他库只能 implements 它,不能通过 extends 继承其实现。这两个修饰符组合后,类型的职责就是定义契约。
类修饰符控制什么
没有修饰符的类默认是开放的:其他库可以构造、继承和实现它。添加修饰符后,可以分别限制这些能力。
下表中的“外部”指声明该类型的库之外。Dart 的库通常对应一个 Dart 文件;通过 part 组成的多个文件仍属于同一个库。
| 声明 | 可直接实例化 | 外部可 extends | 外部可 implements | 主要用途 |
|---|---|---|---|---|
class | 是 | 是 | 是 | 完全开放的类型 |
abstract class | 否 | 是 | 是 | 带有未完成行为的基类 |
base class | 是 | 是 | 否 | 要求子类型继承已有实现 |
interface class | 是 | 否 | 是 | 允许替换实现的公开契约 |
final class | 是 | 否 | 否 | 对外关闭类型层级 |
sealed class | 否 | 否 | 否 | 可穷尽处理的封闭类型族 |
表中的结论针对单独使用修饰符的情况。abstract 还可以与 base、interface 或 final 组合,从而在禁止实例化的同时保留相应的继承约束。
abstract:定义未完成的基类
abstract class 不能直接实例化,可以同时包含抽象成员和已经实现的成员。没有方法体的实例成员是抽象成员,具体子类必须实现它们。
abstract class Shape {
double get area;
void printArea() {
print(area.toStringAsFixed(2));
}
}
final class Circle extends Shape {
Circle(this.radius);
final double radius;
double get area => 3.14159 * radius * radius;
}
void main() {
Circle(2).printArea(); // 12.57
}
Shape 不能被构造,但它提供的 printArea() 会被子类继承。abstract 本身不限制其他库使用 extends 或 implements;如果还需要限制外部子类型,应组合其他修饰符。
base:保证实现被继承
base class 允许其他库继承,但不允许其他库实现。这样可以保证每个外部子类型都经过基类构造函数,并继承基类的实现与私有成员。
base class Vehicle {
Vehicle(this.speed);
final int speed;
String describe() => 'speed: $speed';
}
import 'vehicle.dart';
final class Bicycle extends Vehicle {
Bicycle() : super(20);
}
// 编译错误:外部库不能 implements Vehicle。
// class FakeVehicle implements Vehicle {}
void main() {
print(Bicycle().describe()); // speed: 20
}
直接继承或实现 base 类型的类必须继续声明为 base、final 或 sealed。这项约束会把“外部不能绕过实现”的保证传递到整个子类型层级。示例中的 Bicycle 使用 final,表示它自身也不再允许外部创建子类型。
interface:只向外公开契约
interface class 可以在声明库中像普通类一样使用,但其他库不能继承它,只能实现它。implements 只采用类型的接口,不继承方法体;实现类必须为接口中的实例成员提供自己的实现。
interface class Logger {
void log(String message) {
print('[log] $message');
}
}
import 'logger.dart';
final class ConsoleLogger implements Logger {
void log(String message) {
print('[console] $message');
}
}
// 编译错误:外部库不能 extends Logger。
// final class FileLogger extends Logger {}
当类型只负责声明能力、不打算提供可构造的默认实现时,通常使用 abstract interface class。这比单独使用 interface class 更准确,因为调用方不会误以为应该直接创建接口类型的实例。
final:关闭外部子类型
final class 不允许其他库继承或实现,但类本身仍然可以实例化。它适合实现细节不应被外部扩展、同时又需要公开构造能力的类型。
final class Settings {
const Settings({required this.darkMode});
final bool darkMode;
}
其他库可以创建 Settings,却不能声明 extends Settings 或 implements Settings。声明库内部仍可创建它的子类型;这些直接子类型也必须使用 base、final 或 sealed,以免外部代码间接打破限制。
如果连实例化也要禁止,可以写成 abstract final class。这种组合常用于只包含静态成员的命名空间式 API:
abstract final class Distance {
static const int metersPerKilometer = 1000;
static int kilometersToMeters(int value) {
return value * metersPerKilometer;
}
}
sealed:定义可穷尽处理的类型族
sealed class 隐式为抽象类,不能直接实例化,也不能在声明库之外被继承或实现。它的直接子类型必须与它位于同一个库中,因此分析器可以知道所有直接分支,并检查 switch 是否穷尽。
sealed class ApiResult {}
final class Success extends ApiResult {
Success(this.value);
final String value;
}
final class Failure extends ApiResult {
Failure(this.message);
final String message;
}
String describe(ApiResult result) {
return switch (result) {
Success(:final value) => 'success: $value',
Failure(:final message) => 'failure: $message',
};
}
void main() {
print(describe(Success('saved'))); // success: saved
}
这里不需要 _ 兜底分支。新增 ApiResult 的直接子类型后,未同步更新的 switch 会收到非穷尽错误,从而提醒调用方处理新情况。
sealed 只封闭自身的直接子类型集合,并不会自动封闭这些子类型后续的层级。为直接子类型选择 final、base 或 sealed,可以明确它们是否还能继续派生。
mixin class:同时作为类和 Mixin
mixin class 声明既可以通过构造对象或 extends 当作类使用,也可以通过 with 把实现混入其他类。它必须同时满足普通类和 Mixin 的声明限制。
mixin class Timestamped {
DateTime createdAt = DateTime.now();
}
final class Note with Timestamped {
Note(this.content);
final String content;
}
void main() {
final note = Note('Learn Dart');
print(note.createdAt);
}
如果只需要混入能力,使用 mixin 声明更直接。base mixin 或 base mixin class 可以进一步要求外部使用者保留该 Mixin 的实现。Mixin 的成员约束、on 子句和应用顺序见 Mixins(混入)。
组合修饰符
abstract 可以与部分限制型修饰符组合。组合时先判断能否实例化,再判断外部允许怎样创建子类型。
| 组合 | 表达的约束 |
|---|---|
abstract base class | 不能实例化;外部只能继承,不能实现 |
abstract interface class | 不能实例化;外部只能实现,不能继承 |
abstract final class | 不能实例化;外部不能继承或实现 |
base mixin | 外部应用该 Mixin 的类型必须保留实现约束 |
base mixin class | 同时作为类和 Mixin,并保留 base 约束 |
base、interface、final 和 sealed 表达的是互斥的外部子类型策略,不能互相叠加。sealed 已经隐式包含“不可实例化”的含义,因此不需要再写 abstract sealed class。
如何选择
选择修饰符时,先确定 API 使用者应该拥有哪种能力:
| 设计目标 | 推荐声明 |
|---|---|
| 类型保持完全开放 | class |
| 提供部分实现,等待子类补全 | abstract class |
| 只发布契约,允许调用方替换实现 | abstract interface class |
| 允许调用方扩展,但必须继承已有实现 | base class |
| 允许构造,不允许外部创建任何子类型 | final class |
固定一组分支,并希望 switch 检查是否穷尽 | sealed class |
| 同一份实现既作为类使用,也作为 Mixin 使用 | mixin class |
对公开 API 添加或收紧修饰符可能让已有调用方无法继续继承或实现类型,属于需要谨慎评估的兼容性变更。设计新 API 时应尽早表达边界,不要等到外部已经依赖某种扩展方式后再关闭它。
TypeScript 对比
TypeScript 的 abstract class 与 Dart 的抽象类用途相近,都可以包含抽象成员和具体实现。TypeScript 的 interface 则只存在于静态类型检查阶段;Dart 的 abstract interface class 仍是 Dart 类型系统和运行时类型关系的一部分,可以用于 is 检查。
TypeScript 没有与 Dart 的 base、final 和 sealed 完全对应的类修饰符。TypeScript 可以通过模块导出策略、私有构造函数或判别联合类型实现部分相似目标,但不能直接表达 Dart 基于库边界的继承与实现限制。
常见误区
认为限制在同一个库内也完全生效
base、interface、final 和 sealed 的继承或实现限制主要约束其他库。同一库中的声明彼此协作,拥有更大的自由;但 base 和 final 对直接子类型的修饰符要求仍需遵守。
认为 implements 会复用方法实现
implements 只采用接口,不继承代码。即使 interface class 中的方法已经有方法体,实现类也必须重新实现该实例成员。需要复用实现时,应根据 API 边界选择 extends、Mixin 或组合。
把 final class 与 final 变量混为一谈
变量上的 final 表示变量只能赋值一次;类上的 final 表示其他库不能创建该类的子类型。两者使用同一个关键字,但约束的对象不同。
认为 sealed 的所有后代都必须位于同一库
只有 sealed 类型的直接子类型必须在同一库中。某个直接子类型是否允许在外部继续派生,取决于它自己的修饰符。
小结
abstract控制实例化,并允许声明等待子类实现的成员。base允许外部继承但禁止外部实现,用于保证实现被保留。interface允许外部实现但禁止外部继承,用于发布可替换的契约。final对外关闭整个子类型入口,但仍允许直接构造对象。sealed固定直接子类型集合,并支持穷尽的switch检查。mixin class同时提供类和 Mixin 两种用法;只需要混入时优先使用mixin。- 修饰符的子类型限制以库为边界,设计公开 API 时要同时考虑调用方的扩展方式与兼容性。