面向 API 维护者的类修饰符
Dart 3.0 添加了一些新的 修饰符 ,可以放在类和 mixin声明 上。如果您是库包的作者,这些修饰符可以让您更好地控制用户可以使用您的包导出的类型做什么。这可以使您的包更容易演进,也更容易知道您的代码更改是否会破坏用户。
Dart 3.0 还包含一个关于使用类作为 mixin 的 重大更改 。此更改可能不会破坏 您的 类,但它可能会破坏您的类的 用户 。
本指南将引导您完成这些更改,以便您了解如何使用新的修饰符以及它们如何影响您的库的用户。
类上的 mixin
修饰符
#最重要的是 mixin
修饰符。Dart 3.0 之前的语言版本允许任何类用作另一个类的 with
子句中的 mixin, 除非 该类:
- 声明任何非工厂构造函数。
- 扩展除
Object
之外的任何类。
这很容易意外地破坏其他人的代码,通过添加构造函数或 extends
子句到一个类,而没有意识到其他人正在 with
子句中使用它。
Dart 3.0 默认情况下不再允许将类用作 mixin。相反,您必须通过声明 mixin class
来显式选择该行为:
mixin class Both {}
class UseAsMixin with Both {}
class UseAsSuperclass extends Both {}
如果您将包更新到 Dart 3.0 并且没有更改任何代码,您可能看不到任何错误。但是,如果用户将您的类用作 mixin,您可能会无意中破坏他们的包。
将类迁移为 mixin
#如果类具有非工厂构造函数、 extends
子句或 with
子句,则它已经不能用作 mixin。Dart 3.0 的行为不会改变;无需担心,也不需要做任何事情。
实际上,这描述了大约 90% 的现有类。对于其余可以用作 mixin 的类,您必须决定要支持什么。
以下是一些帮助您做出决定的问题。首先是务实的:
- 您想冒破坏任何用户的风险吗? 如果答案是坚决的“不”,则在任何和所有 可以用作 mixin 的类之前放置
mixin
。这精确地保留了您 API 的现有行为。
另一方面,如果您想借此机会重新考虑您的 API 提供的功能,那么您可能不想将其转换为 mixin class
。考虑这两个设计问题:
您是否希望用户能够直接构造它的实例? 换句话说,这个类是不是故意不是抽象的?
**您是否 希望 人们能够将声明用作 mixin?**换句话说,您是否希望他们能够在
with
子句中使用它?
如果两个答案都是“是”,则将其设为 mixin 类。如果第二个答案是“否”,则将其保留为类。如果第一个答案是“否”,第二个答案是“是”,则将其从类更改为 mixin 声明。
最后两个选项,将其保留为类或将其转换为纯 mixin,都是破坏性 API 更改。如果您这样做,您需要增加包的主要版本号。
其他选择加入修饰符
#处理类作为 mixin 是 Dart 3.0 中唯一会影响包 API 的关键更改。一旦您完成了这一步,如果您不想对包允许用户执行的操作进行其他更改,则可以停止。
请注意,如果您继续并使用下面描述的任何修饰符,则可能会对包的 API 造成破坏性更改,这需要增加主要版本号。
interface
修饰符
#Dart 没有单独的语法来声明纯接口。相反,您声明一个碰巧只包含抽象方法的抽象类。当用户在包的 API 中看到该类时,他们可能不知道它是否包含可以通过扩展类重用的代码,或者它是否旨在用作接口。
您可以通过在类上添加 interface
修饰符来阐明这一点。这允许类在 implements
子句中使用,但阻止它在 extends
中使用。
即使类 确实 具有非抽象方法,您也可能希望阻止用户扩展它。继承是软件中最强大的耦合类型之一,因为它可以实现代码重用。但是这种耦合也很 危险且脆弱 。当继承跨越包边界时,很难在不破坏子类的情况下演进超类。
将类标记为 interface
允许用户构造它(除非它 也标记为 abstract
)并实现类的接口,但阻止他们重用其任何代码。
当一个类被标记为 interface
时,可以在声明类的库中忽略此限制。在库内部,您可以自由地扩展它,因为它是您所有的代码,并且您大概知道自己在做什么。此限制适用于其他包,甚至适用于您自己包中的其他库。
base
修饰符
#base
修饰符与 interface
有些相反。它允许您在 extends
子句中使用该类,或在 with
子句中使用 mixin 或 mixin 类。但是,它不允许类库外部的代码在 implements
子句中使用该类或 mixin。
这确保了作为类或 mixin 接口实例的每个对象都继承了您的实际实现。特别是,这意味着每个实例都将包含类或 mixin 声明的所有私有成员。这有助于防止可能发生的运行时错误。
考虑这个库:
class A {
void _privateMethod() {
print('我继承自 A');
}
}
void callPrivateMethod(A a) {
a._privateMethod();
}
这段代码本身看起来不错,但是没有什么可以阻止用户创建另一个像这样的库:
import 'a.dart';
class B implements A {
// 没有 _privateMethod() 的实现!
}
main() {
callPrivateMethod(B()); // 运行时异常!
}
向类添加 base
修饰符可以帮助防止这些运行时错误。与 interface
一样,您可以在声明 base
类或 mixin 的同一库中忽略此限制。然后,同一库中的子类将被提醒实现私有方法。但请注意,下一节 确实 适用:
Base 传递性
#标记类为 base
的目标是确保该类型的每个实例都具体地继承自它。为维护这一点,base 限制是“具有传染性的”。每个标记为 base
的类型的子类型—— 直接或间接 ——也必须阻止被实现。这意味着它必须标记为 base
(或 final
或 sealed
,我们将在后面介绍)。
因此,将 base
应用于类型需要一些小心。它不仅会影响用户可以使用您的类或 mixin 的方式,还会影响 它们的 子类可以提供的功能。一旦您在类型上放置了 base
,其下的整个层次结构都将被禁止实现。
这听起来很强烈,但这正是大多数其他编程语言一直以来的工作方式。大多数根本没有隐式接口,因此当您在 Java、C# 或其他语言中声明一个类时,您实际上具有相同的约束。
final
修饰符
#如果您想要 interface
和 base
的所有限制,您可以将类或 mixin 类标记为 final
。这阻止库外部的任何人创建任何类型的子类型:不在 implements
、 extends
、 with
或 on
子句中使用它。
这对类的用户来说是最严格的。他们唯一能做的就是构造它(除非它被标记为 abstract
)。作为回报,您作为类维护者受到的限制最少。您可以添加新方法,将构造函数转换为工厂构造函数等,而无需担心破坏任何下游用户。
sealed
修饰符
#最后一个修饰符, sealed
,是特殊的。它主要用于在模式匹配中启用 穷举检查 。如果 switch 具有针对标记为 sealed
的类型的每个直接子类型的 case,则编译器知道该 switch 是穷举的。
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',
};
此 switch 具有针对 Amigo
的每个子类型的 case。编译器知道 Amigo
的每个实例都必须是这些子类型之一的实例,因此它知道该 switch 是安全穷举的,不需要任何最终的默认 case。
为了使这合理,编译器强制执行两个限制:
密封类本身不能直接构造。否则,您可以拥有一个不是任何子类型的
Amigo
实例。因此,每个sealed
类也隐式地是abstract
。密封类型的每个直接子类型都必须位于声明密封类型的同一库中。这样,编译器就可以找到它们。它知道没有其他隐藏的子类型会与任何 case 不匹配。
第二个限制类似于 final
。与 final
一样,这意味着标记为 sealed
的类不能在其声明的库外部直接扩展、实现或混合使用。但是,与 base
和 final
不同,没有 传递 限制:
sealed class Amigo {}
class Lucky extends Amigo {}
class Dusty extends Amigo {}
class Ned extends Amigo {}
// 这是一个错误:
class Bad extends Amigo {}
// 但这两个都没问题:
class OtherLucky extends Lucky {}
class OtherDusty implements Dusty {}
当然,如果您 希望 您的密封类型的子类型也受到限制,您可以通过使用 interface
、 base
、 final
或 sealed
来标记它们来实现。
sealed
与 final
#如果您有一个不想让用户直接子类型的类,那么您应该何时使用 sealed
与 final
?一些简单的规则:
如果您希望用户能够直接构造类的实例,则它 不能 使用
sealed
,因为密封类型隐式地是抽象的。如果类在您的库中没有子类型,则使用
sealed
没有任何意义,因为您不会获得任何穷举检查的好处。
否则,如果类确实有一些您定义的子类型,那么 sealed
可能就是您想要的。如果用户看到该类有一些子类型,则能够分别处理每个子类型作为 switch case 并让编译器知道整个类型都被覆盖是很方便的。
使用 sealed
确实意味着,如果您稍后向库中添加另一个子类型,则这是一个破坏性 API 更改。当出现新的子类型时,所有这些现有的 switch 都变得不穷举,因为它们不处理新类型。这与向枚举添加新值完全一样。
这些不穷举的 switch 编译错误对用户来说是 有用的 ,因为它们会将用户的注意力吸引到他们需要处理新类型的代码位置。 但这确实意味着每当您添加新的子类型时,它都是一项重大更改。如果您希望能够以非破坏性的方式添加新的子类型,那么最好使用 final
而不是 sealed
来标记超类型。这意味着当用户切换该超类型的某个值时,即使他们为所有子类型都设置了 case,编译器也会强制他们添加另一个默认 case。如果您以后添加更多子类型,则将执行该默认 case。
总结
#作为 API 设计者,这些新的修饰符可以让您控制用户如何使用您的代码,反过来,您也可以在不破坏用户代码的情况下改进代码。
但是这些选项也带来了复杂性:您现在作为 API 设计者需要做出更多选择。此外,由于这些功能是新的,我们仍然不知道最佳实践是什么。每种语言的生态系统都不同,需求也不同。
幸运的是,您不需要一次性解决所有问题。我们故意选择了默认值,因此即使您什么都不做,您的类也大多具有与 3.0 之前相同的特性。如果您只想保持 API 的原样,请在已经支持该功能的类上添加 mixin
,然后就完成了。
随着时间的推移,当您了解到需要更精细的控制的地方时,您可以考虑应用其他一些修饰符:
使用
interface
来阻止用户重用类的代码,同时允许他们重新实现其接口。使用
base
来要求用户重用类的代码,并确保类的每个实例都是该实际类或子类的实例。使用
final
来完全阻止扩展类。使用
sealed
来选择加入对一系列子类型的穷举检查。
当您这样做时,发布包时请递增主要版本号,因为这些修饰符都暗示着构成重大更改的限制。
除非另有说明,否则本网站上的文档反映的是 Dart 3.6.0。页面最后更新于 2025-02-05。 查看源代码 或 报告问题.