API 维护者的类修饰符

Last updated: ... / Reads: 35 Edit

Dart 3.0 添加了一些新的修饰符,你可以将它们放在 class 和 mixin 声明中。 如果您是库包的作者, 通过这些修饰符,您可以更好地控制允许用户执行的操作 替换为包导出的类型。 这可以使你的包更容易发展, 并且更容易知道对代码的更改是否会破坏用户。

Dart 3.0 还包括一个关于使用类作为混合的重大变化。 这个变化可能不会打断你的班级, 但它可能会破坏您班级的用户。

本指南将指导您完成这些更改 所以你知道如何使用新的修饰符, 以及它们如何影响库的用户。

类的修饰符mixin

需要注意的最重要的修饰符是 。 Dart 3.0 之前的语言版本允许将任何类用作 mixin 在另一个类的子句中,除非该类:mixinwith

  • 声明任何非工厂构造函数。
  • 扩展除 以外的任何类。Object

这使得不小心破坏别人的代码变得容易, 通过向类添加构造函数或子句 而没有意识到其他人正在从句中使用它。extendswith

Dart 3.0 不再默认允许将类用作 mixin。 相反,您必须通过声明以下命令来显式选择加入该行为:mixin class

mixin class Both {}

class UseAsMixin with Both {}
class UseAsSuperclass extends Both {}

如果你将软件包更新到 Dart 3.0 并且不更改任何代码, 您可能不会看到任何错误。 但你可能会无意中破坏你的包的用户 如果他们将您的类用作混合。

将类迁移为 mixin

如果该类具有非工厂构造函数,则子句 或者一个子句,那么它已经不能用作 mixin。 行为不会随着 Dart 3.0 而改变; 没有什么可担心的,也不需要做任何事情。extendswith

在实践中,这描述了大约 90% 的现有类。 对于其余可用作 mixin 的类, 你必须决定你想支持什么。

以下是一些有助于做出决定的问题。首先是务实的:

您想冒着破坏任何用户的风险吗?如果答案是硬性“否”, 然后放在任何和所有可以用作 mixin 的类之前。 这完全保留了 API 的现有行为。mixin

另一方面,如果你想借此机会重新思考 提供您的 API 提供的功能,那么您可能不希望将其转换为 .请考虑以下两个设计问题:mixin class

您是否希望用户能够直接构建它的实例?换句话说,类是故意不抽象的吗?

你希望人们能够将声明用作混合吗?换句话说,您是否希望他们能够在子句中使用它?with

如果两者的答案都是“是”,那么就把它设成一个 mixin 类。如果答案 第二个是“不”,然后把它作为一个班级。如果第一个答案 是“no”,第二个是“yes”,然后将其从类更改为 mixin 声明。

最后两个选项,让它成为一个类或把它变成一个纯粹的混合, 正在中断 API 更改。您需要提升包的主要版本 如果你这样做。

其他选择加入修饰符

将类作为 mixin 处理是 Dart 3.0 中唯一的关键变化 这会影响包的 API。一旦你走到这一步, 如果您不想进行其他更改,可以停止 到您的包允许用户执行的操作。

请注意,如果您继续并使用下面描述的任何修饰符, 这可能是对包的 API 的重大更改,因此需要 主要版本增量。

修饰符interface

Dart 没有单独的语法来声明纯接口。 相反,您声明了一个恰好只包含的抽象类 抽象方法。 当用户在包的 API 中看到该类时, 他们可能不知道它是否包含可以通过扩展类来重用的代码, 或者它是否打算用作接口。

您可以通过在类上放置接口修饰符来阐明这一点。 这允许在子句中使用该类, 但阻止它在 .implementsextends

即使该类确实具有非抽象方法,您可能也希望阻止 用户从扩展它。 继承是软件中最强大的耦合类型之一, 因为它支持代码重用。 但这种耦合也是危险和脆弱的。 当继承跨越包边界时, 在不破坏子类的情况下发展超类是很困难的。

标记类允许用户构造它(除非它也被标记为抽象) 并实现类的接口, 但阻止他们重用其任何代码。interface

当一个类被标记时,可以在 声明类的库。 在库中,您可以自由扩展它,因为它是您的所有代码 想必你知道你在做什么。 该限制适用于其他软件包, 甚至是您自己的包中的其他库。interface

修饰符base

基本修饰符在某种程度上与 相反。 它允许您在子句中使用该类, 或者在子句中使用 mixin 或 mixin 类。 但是,它不允许类库之外的代码 在子句中使用类或 mixin。interfaceextendswithimplements

这确保了作为实例的每个对象 你的类或 mixin 的接口继承了你的实际实现。 具体而言,这意味着每个实例都将包括 您的班级或 mixin 声明的所有私有成员。 这有助于防止可能发生的运行时错误。

请考虑以下库:

// a.dart
class A {
  void _privateMethod() {
    print('I inherited from A');
  }
}

void callPrivateMethod(A a) {
  a._privateMethod();
}

这段代码本身似乎很好, 但是,没有什么可以阻止用户创建另一个库,如下所示:

// b.dart
import 'a.dart';

class B implements A {
  // No implementation of _privateMethod()!
}

main() {
  callPrivateMethod(B()); // Runtime exception!
}

将修饰符添加到类有助于防止这些运行时错误。 与 一样,您可以忽略此限制 在声明类或 mixin 的同一库中。 然后在同一库中的子类 将提醒实现私有方法。 但请注意,下一节确实适用:baseinterfacebase

基本传递性

标记类的目标是确保 该类型的每个实例都具体地继承自它。 为了保持这一点,基本限制是“传染性的”。 标记类型的每个子类型 - 直接或间接 - 还必须防止被实施。 这意味着它必须被标记(或或,我们接下来将讨论)。basebasebasefinalsealed

因此,应用于一种类型需要一些小心。 它不仅影响用户对你的类或混音可以做什么, 还有他们的子类可以提供的便利性。 一旦你穿上了类型,它下面的整个层次结构 被禁止实施。basebase

这听起来很激烈,但这是大多数其他编程语言的方式 一直有效。 大多数根本没有隐式接口, 因此,当您使用 Java、C# 或其他语言声明类时, 您实际上具有相同的约束。

修饰符final

如果您想要两者的所有限制 和 , 您可以将课程或 mixin 课程标记为决赛。 这样可以防止库外的任何人创建 它的任何类型的子类型: 不要在 、 、 或子句中使用它。interfacebaseimplementsextendswithon

对于该类的用户来说,这是最严格的。 他们所能做的就是构造它(除非它被标记)。 作为回报,作为类维护者,您拥有最少的限制。 您可以添加新方法,将构造函数转换为工厂构造函数等。 无需担心破坏任何下游用户。abstract

修饰符sealed

最后一个修饰符,密封的,很特别。 它的存在主要是为了在模式匹配中启用详尽性检查。 如果开关对标记为 、 的类型的每个直接子类型都有大小写 , 然后编译器知道开关是详尽的。sealed

// amigos.dart
sealed class Amigo {}

class Lucky extends Amigo {}

class Dusty extends Amigo {}

class Ned extends Amigo {}

String lastName(Amigo amigo) => switch (amigo) {
      Lucky _ => 'Day',
      Dusty _ => 'Bottoms',
      Ned _ => 'Nederlander',
    };

此开关对 的每个子类型都有一个大小写。 编译器知道 的每个实例都必须是 1 的实例 这些子类型,因此它知道交换机是安全的详尽的,而不是 需要任何最终的默认情况。AmigoAmigo

为了合理起见,编译器强制执行两个限制:

  1. 密封类本身不能直接构造。 否则,您可能有一个实例不是 任何子类型的实例。 所以每个类也是隐含的。Amigosealedabstract
  2. 密封类型的每个直接子类型都必须位于同一库中 其中声明了密封类型。 这样,编译器就可以找到它们。它知道没有 其他隐藏的子类型漂浮在周围,与任何情况都不匹配。

第二个限制类似于 。 像 ,表示标记的类不能直接 在声明它的库之外扩展、实现或混合。 但是,与 不同的是,没有传递限制:finalfinalsealedbasefinal

// amigo.dart
sealed class Amigo {}
class Lucky extends Amigo {}
class Dusty extends Amigo {}
class Ned extends Amigo {}

// other.dart

// This is an error:
class Bad extends Amigo {}

// But these are both fine:
class OtherLucky extends Lucky {}
class OtherDusty implements Dusty {}

当然,如果您想要密封类型的子类型 要受到限制,您可以通过标记它们来获得它 使用 、 、 或 。interfacebasefinalsealed

sealed对final

如果您不希望用户能够直接对类进行子类型化, 什么时候应该使用 VS ? 几个简单的规则:sealedfinal

如果希望用户能够直接构造类的实例, 那么它就不能使用,因为密封类型是隐式抽象的。sealed

如果该类在您的库中没有子类型,那么使用就没有意义,因为您没有获得详尽性检查的好处。sealed

否则,如果类确实具有您定义的某些子类型, 那么很可能是你想要的。 如果用户看到该类有几个子类型,那么能够很方便 将它们中的每一个分别作为开关案例处理 并让编译器知道整个类型都已覆盖。sealed

使用 确实意味着,如果以后将另一个子类型添加到库中, 这是一项重大的 API 更改。 当出现新的子类型时, 所有这些现有开关都变得不详尽 因为他们不处理新类型。 这就像向枚举添加新值一样。sealed

这些非详尽的交换机编译错误对用户很有用 因为它们将用户的注意力吸引到代码中的位置 他们需要处理新类型的地方。

但这确实意味着,每当您添加新的子类型时,它都是一个重大更改。 如果您希望以不间断的方式自由添加新的子类型, 那么最好用 而不是 . 这意味着,当用户打开该超类型的值时, 即使他们有所有亚型的案例, 编译器将强制他们添加另一个默认大小写。 如果稍后添加更多子类型,则将执行该默认情况。finalsealed

总结

作为 API 设计者, 这些新的修饰符使你能够控制用户使用代码的方式。 反过来说,你如何在不破坏他们的代码的情况下发展你的代码。

但这些选项也带来了复杂性: 作为 API 设计人员,您现在有更多选择。 此外,由于这些功能是新功能, 我们仍然不知道最佳实践是什么。 每种语言的生态系统都是不同的,有不同的需求。

幸运的是,您不需要一下子弄清楚。 我们特意选择了默认值,这样即使你什么都不做, 您的课程大多具有与 3.0 之前相同的功能。 如果你只是想保持你的 API 原样, 把已经支持它的类放上去,你就完成了。mixin

随着时间的流逝,当您了解自己想要更精细控制的位置时, 您可以考虑应用一些其他修饰符:

用于防止用户重复使用类的代码 同时允许他们重新实现其接口。interface

用于要求用户重用类的代码 并确保类类型的每个实例都是一个实例 该实际类或子类。base

用于完全阻止扩展类。final

用于选择对一系列子类型进行详尽性检查。sealed

执行此操作时,请在发布包时递增主要版本。 因为这些修饰符都暗示了破坏性更改的限制。


Comments

Make a comment