
前言
一家公司提供了一套设备控制代码。使用者可以直接创建它提供的设备对象,也可能想在原有代码上增加功能,或者完全自己写一套,只遵守相同的调用方式。这三种做法并不一样:创建对象、继承实现、实现接口,分别决定了使用者能做什么。
如果代码作者希望别人只能使用现成实现,或允许扩展却不允许重写,单靠约定很难保证。Dart 的类修饰符就写在类声明前,让这些规则成为语言能够检查的约束。
一、先弄清三种操作
**类修饰符是放在类声明前的关键字,用来限制其他代码如何使用这个类。**理解修饰符前,先分清三个常见动作:
dart
class Device {
void start() => print('启动');
}
class SmartDevice extends Device {}
class DemoDevice implements Device {
@override
void start() => print('演示启动');
}
void main() {
final device = Device();
device.start();
}
- Device() 是创建对象:直接使用 Device 提供的实现。
- extends 是继承:SmartDevice 得到 Device 的实例成员实现,还能增加或改写功能。
- implements 是实现接口:DemoDevice 承诺提供 Device 对外要求的成员,但不会继承 start() 的方法体,必须自己实现。
可以把它们理解为"直接使用成品""在成品上继续改造""照着功能清单自己制作"。这个联系只帮助区分三种操作;真正的规则仍由 Dart 的类声明和库边界决定。Dart 类
一个类如果没有写修饰符,其他库默认可以创建它的对象、继承它、实现它。实际能否创建对象还取决于是否存在可访问的构造函数。修饰符控制的重点是库外代码 :同一个库内部往往仍可继承或实现被限制的类。库也不等于文件数量;多个文件可以通过 part 属于同一个库。下文说的"外部",都是指另一个库 ,即使它和原文件放在同一个项目里。Dart 库与导入
二、abstract:不能直接创建,还可以继续定义子类
**抽象类是不能直接创建对象的类。**它适合表达一类对象共同拥有的能力,却不规定每个细节怎样完成:
dart
abstract class Transport {
void move();
}
class Bicycle extends Transport {
@override
void move() => print('脚踏前进');
}
void main() {
// Transport() 不能直接创建。
final transport = Bicycle();
transport.move();
}
Transport 声明了 move(),但没有方法体,因此不能直接拿它运行;Bicycle 补全了做法。抽象类也可以包含已经写好的方法,供子类继承。abstract 限制的是"直接创建",并不禁止库外 extends 或 implements 。所以"抽象"与"是否允许别人继承"是两个不同问题。Dart 类修饰符:abstract
三、interface 与 base:分别允许哪种扩展
写一个供别人使用的库时,作者常常要在两种合作方式之间选择:让使用者自己实现同一套功能,还是要求他们从已有实现继续扩展。
3.1 interface:可以照接口实现,不能继承现成代码
interface class 允许其他库 implements,但不允许其他库 extends。
dart
// device.dart
interface class Device {
void start() => print('默认启动');
}
dart
// main.dart
import 'device.dart';
class CustomDevice implements Device {
@override
void start() => print('自定义启动');
}
void main() {
Device().start();
CustomDevice().start();
}
main.dart 是另一个库。它可以创建 Device 对象,也可以用 implements 写 CustomDevice;CustomDevice 必须自己提供 start(),不能直接拿走 Device 的方法体。如果把 implements 改成 extends,编译器会报错。库作者借此告诉使用者:可以遵守这套调用约定,但不要依赖我的内部实现来派生子类 。Dart 类修饰符:interface
如果这套约定本身不应该被直接创建,可组合成 abstract interface class。这样,库外仍可 implements,却不能 extends,也不能直接创建这个抽象类。abstract 负责"不可直接创建",interface 负责"库外不可继承",两个修饰符各管一件事。
3.2 base:可以继承现成代码,不能只照接口重写
base class 允许其他库 extends,但不允许其他库 implements。
dart
// counter.dart
base class Counter {
int value = 0;
void increase() => value++;
}
dart
// main.dart
import 'counter.dart';
base class LimitedCounter extends Counter {
@override
void increase() {
if (value < 10) super.increase();
}
}
void main() {
final counter = LimitedCounter();
counter.increase();
print(counter.value); // 1
}
LimitedCounter 真正继承了 Counter 的 value 和 increase(),再加上"不超过 10"的限制。这样,凡是符合 Counter 类型的对象,都必须带有 Counter 的实际实现;其他库不能只写 implements Counter,跳过这份实现。Dart 类修饰符:base
注意子类声明前也写了 base。这不是为了好看:如果普通子类继承了 base 类,外部又能 implements 那个普通子类,就会绕过原本要求继承的实现。Dart 因此要求继承或实现 base 类的类继续使用 base、final 或 sealed,限制会沿类型层次传下去。相较之下,interface 不会自动要求实现类继续声明为 interface。
四、final 与 sealed:都限制库外扩展,但用途不同
4.1 final:库外只能使用,不能派生新类型
**final class 不允许其他库 extends,也不允许其他库 implements。**只要有可访问的构造函数,其他库仍然可以创建并使用它的对象。
dart
// token.dart
final class Token {
final String text;
Token(this.text);
}
dart
// main.dart
import 'token.dart';
void main() {
print(Token('abc').text); // abc
}
把"不能扩展"误读为"不能使用",会错过 final 的重点。库作者可以公开 Token 给别人创建和调用,同时决定只有本库能定义它的子类型。这里的 final 修饰类 ;字段前的 final 则表示该变量只能赋值一次,二者不能混为一谈。同一个库内部仍可定义 final 类的子类,但这些子类也须遵守 base、final 或 sealed 的传递限制。Dart 类修饰符:final
4.2 sealed:让可能的直接子类型可数清
**sealed class 把直接子类型限制在同一个库内,并且自身不能直接创建对象。**这让编译器能够列出一组已知情况,检查 switch 是否漏掉其中一种。
dart
sealed class LoadResult {}
final class Loaded extends LoadResult {
final String data;
Loaded(this.data);
}
final class Failed extends LoadResult {
final String reason;
Failed(this.reason);
}
String describe(LoadResult result) => switch (result) {
Loaded(data: final data) => '已加载:$data',
Failed(reason: final reason) => '加载失败:$reason',
};
void main() {
print(describe(Loaded('照片')));
print(describe(Failed('网络不可用')));
}
LoadResult 只有同库声明的 Loaded 和 Failed 两个直接子类型。switch 分别处理了两种情况,所以不需要再写兜底分支。若后来在这个库里添加第三种直接子类型,原来的 switch 就可能不再穷尽,编译器会提醒补上处理。这个提醒正是 sealed 的价值:新增情况时,遗漏不会悄悄溜过去。Dart 类修饰符:sealed
这里把 Loaded、Failed 也写成 final,是为了让它们不能在库外继续派生。**sealed 只限制对自身的直接继承和实现,不会自动封住每个子类。**如果一个普通子类还允许外部扩展,匹配到这个子类的分支仍可覆盖其派生对象,但"整个类型层次只有这两个具体类"就不能这样说。
final 与 sealed 都不允许其他库直接派生自己的子类型;区别在于:final 类可以直接创建,且不让编译器仅凭列出的子类型推断 switch 已穷尽;sealed 类本身不可创建,适合明确列举多种结果。若未来可能新增子类型,又不希望每次新增都让使用者的 switch 必须修改,final 通常更合适。Dart 类修饰符与 API 设计
五、mixin class:类也能用于 with,需要明确声明
extends 与 implements 之外,还有 with:把可复用的行为混入类中。Dart 3 起,普通 class 不再自动允许当作 mixin 使用;如果一个声明既要像普通类一样可创建、可继承,又要能被 with 使用,可以写 mixin class:
dart
mixin class Timestamped {
String timestamp() => DateTime.now().toIso8601String();
}
class Report with Timestamped {}
void main() {
print(Report().timestamp());
}
mixin class 与单纯的 mixin 不相同:前者也是类,允许像类一样创建对象;后者主要用于混入。若只需要复用行为,无须把它作为普通类创建,就不必为了"更全"写成 mixin class。base 也可用于 mixin 声明,以要求外部使用者保留其实现。关于 with 的细节,应结合混入本身理解,不要把它与 extends、implements 当成同一种操作。Dart 混入
六、怎样选择修饰符
先问两个问题:是否允许库外继承实现?是否允许库外只实现接口?再问是否允许直接创建对象。这样比死记几个英文词更可靠。
| 声明 | 库外直接创建 | 库外 extends | 库外 implements | 主要意图 |
|---|---|---|---|---|
| class | 可以* | 可以 | 可以 | 不施加额外限制 |
| abstract class | 不可以 | 可以 | 可以 | 留待子类补全或复用 |
| interface class | 可以* | 不可以 | 可以 | 只开放接口实现 |
| base class | 可以* | 可以 | 不可以 | 必须继承实际实现 |
| final class | 可以* | 不可以 | 不可以 | 关闭库外类型扩展 |
| sealed class | 不可以 | 不可以 | 不可以 | 在库内列出直接子类型 |
*"可以"以存在可访问的构造函数为前提;abstract interface class 等组合会进一步限制创建。表中只讨论库外的直接操作,不代表同库代码也受同样限制。
类修饰符改变的是库的对外承诺。已公开的类如果从普通 class 改成 interface、base 或 final,原本使用 extends 或 implements 的代码可能立刻编译失败;给库增加限制时,要考虑现有使用者。反过来,不确定是否需要限制时,先搞清楚自己想开放哪种扩展方式,再选择关键字,而不是把 final 当作默认的"更安全"。
七、总结
类修饰符回答三件事:能否直接创建、能否继承实现、能否只实现接口。abstract 让类不能直接创建;interface 允许库外实现而禁止继承;base 允许继承而禁止库外单独实现;final 禁止库外派生子类型;sealed 还让库内的直接子类型能够用于穷尽匹配。mixin class 则明确允许一个类被 with 使用。
先辨认代码是在同一个库还是另一个库,再辨认使用的是创建、extends、implements 还是 with,修饰符的含义就不会混成一团。
八、测试题
- 一个库公开 interface class Printer,另一个库能用 extends Printer 吗?能用 implements Printer 吗?实现者能直接继承 Printer 的方法体吗?
- 把 base class Counter 的子类写成普通 class AdvancedCounter,为什么 Dart 不允许?
- final class Ticket 有公开构造函数。其他库能创建 Ticket 对象吗?能声明 class SpecialTicket extends Ticket 吗?
- sealed class Result 在同一库中有 Success、Failure 两个直接子类型。switch 处理了这两个类型后,为什么可以不写兜底?如果再增加一个直接子类型,会怎样?
- abstract interface class Service 与 interface class Service 相比,额外限制了什么?