跳到主要内容

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 还可以与 baseinterfacefinal 组合,从而在禁止实例化的同时保留相应的继承约束。

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 本身不限制其他库使用 extendsimplements;如果还需要限制外部子类型,应组合其他修饰符。

base:保证实现被继承

base class 允许其他库继承,但不允许其他库实现。这样可以保证每个外部子类型都经过基类构造函数,并继承基类的实现与私有成员。

vehicle.dart
base class Vehicle {
Vehicle(this.speed);

final int speed;

String describe() => 'speed: $speed';
}
bicycle.dart
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 类型的类必须继续声明为 basefinalsealed。这项约束会把“外部不能绕过实现”的保证传递到整个子类型层级。示例中的 Bicycle 使用 final,表示它自身也不再允许外部创建子类型。

interface:只向外公开契约

interface class 可以在声明库中像普通类一样使用,但其他库不能继承它,只能实现它。implements 只采用类型的接口,不继承方法体;实现类必须为接口中的实例成员提供自己的实现。

logger.dart
interface class Logger {
void log(String message) {
print('[log] $message');
}
}
console_logger.dart
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 不允许其他库继承或实现,但类本身仍然可以实例化。它适合实现细节不应被外部扩展、同时又需要公开构造能力的类型。

settings.dart
final class Settings {
const Settings({required this.darkMode});

final bool darkMode;
}

其他库可以创建 Settings,却不能声明 extends Settingsimplements Settings。声明库内部仍可创建它的子类型;这些直接子类型也必须使用 basefinalsealed,以免外部代码间接打破限制。

如果连实例化也要禁止,可以写成 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 只封闭自身的直接子类型集合,并不会自动封闭这些子类型后续的层级。为直接子类型选择 finalbasesealed,可以明确它们是否还能继续派生。

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 mixinbase mixin class 可以进一步要求外部使用者保留该 Mixin 的实现。Mixin 的成员约束、on 子句和应用顺序见 Mixins(混入)

组合修饰符

abstract 可以与部分限制型修饰符组合。组合时先判断能否实例化,再判断外部允许怎样创建子类型。

组合表达的约束
abstract base class不能实例化;外部只能继承,不能实现
abstract interface class不能实例化;外部只能实现,不能继承
abstract final class不能实例化;外部不能继承或实现
base mixin外部应用该 Mixin 的类型必须保留实现约束
base mixin class同时作为类和 Mixin,并保留 base 约束

baseinterfacefinalsealed 表达的是互斥的外部子类型策略,不能互相叠加。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 的 basefinalsealed 完全对应的类修饰符。TypeScript 可以通过模块导出策略、私有构造函数或判别联合类型实现部分相似目标,但不能直接表达 Dart 基于库边界的继承与实现限制。

常见误区

认为限制在同一个库内也完全生效

baseinterfacefinalsealed 的继承或实现限制主要约束其他库。同一库中的声明彼此协作,拥有更大的自由;但 basefinal 对直接子类型的修饰符要求仍需遵守。

认为 implements 会复用方法实现

implements 只采用接口,不继承代码。即使 interface class 中的方法已经有方法体,实现类也必须重新实现该实例成员。需要复用实现时,应根据 API 边界选择 extends、Mixin 或组合。

final classfinal 变量混为一谈

变量上的 final 表示变量只能赋值一次;类上的 final 表示其他库不能创建该类的子类型。两者使用同一个关键字,但约束的对象不同。

认为 sealed 的所有后代都必须位于同一库

只有 sealed 类型的直接子类型必须在同一库中。某个直接子类型是否允许在外部继续派生,取决于它自己的修饰符。

小结

  • abstract 控制实例化,并允许声明等待子类实现的成员。
  • base 允许外部继承但禁止外部实现,用于保证实现被保留。
  • interface 允许外部实现但禁止外部继承,用于发布可替换的契约。
  • final 对外关闭整个子类型入口,但仍允许直接构造对象。
  • sealed 固定直接子类型集合,并支持穷尽的 switch 检查。
  • mixin class 同时提供类和 Mixin 两种用法;只需要混入时优先使用 mixin
  • 修饰符的子类型限制以库为边界,设计公开 API 时要同时考虑调用方的扩展方式与兼容性。