Dart 之类修饰符

前言

一家公司提供了一套设备控制代码。使用者可以直接创建它提供的设备对象,也可能想在原有代码上增加功能,或者完全自己写一套,只遵守相同的调用方式。这三种做法并不一样:创建对象、继承实现、实现接口,分别决定了使用者能做什么。

如果代码作者希望别人只能使用现成实现,或允许扩展却不允许重写,单靠约定很难保证。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,修饰符的含义就不会混成一团。

八、测试题

  1. 一个库公开 interface class Printer,另一个库能用 extends Printer 吗?能用 implements Printer 吗?实现者能直接继承 Printer 的方法体吗?
  2. 把 base class Counter 的子类写成普通 class AdvancedCounter,为什么 Dart 不允许?
  3. final class Ticket 有公开构造函数。其他库能创建 Ticket 对象吗?能声明 class SpecialTicket extends Ticket 吗?
  4. sealed class Result 在同一库中有 Success、Failure 两个直接子类型。switch 处理了这两个类型后,为什么可以不写兜底?如果再增加一个直接子类型,会怎样?
  5. abstract interface class Service 与 interface class Service 相比,额外限制了什么?
相关推荐
YB13751 小时前
Jetpack Compose 动画Modifier.animateContentSize()
android
事圆则缓1 小时前
Android 版本号、API Level 和 JDK 版本有什么关系?
android
zhangguojia71 小时前
Android Activity 生命周期结束流程:从 finish() 到 onDestroy() 的跨进程状态调度
android
Jewel1052 小时前
文字为什么总是不居中!记录一个bug
android·flutter·ios
mmsx2 小时前
Android 并发考古:newThread 与 RxJava 共存,以及 GlobalScope 的三宗罪
android·echarts·rxjava
骑着蜗牛撵大象3272 小时前
大模型 SDK 编译就崩?用 CMakeLists 把 httplib、JSON 与 OpenSSL 装配成可复用的 ChatServer
android·java·c++·http·json·cmake·大模型sdk
海水冷却12 小时前
Android 群组聊天怎么实现:基于 ZIM 的完整落地指南(附可运行 Demo)
android
三少爷的鞋14 小时前
现代 Android 架构指南:不要再主动设计 Callback 接口
android
TimeFine20 小时前
AI 编码工作流的设计
android·ai编程