9.1.3 选择启用(opt-in)要求
原文链接: https://kotlinlang.org/docs/opt-in-requirements.html
9.1.3 选择启用(opt-in)要求
Kotlin 标准库提供了一种机制,用于要求并获取使用某些 API 元素的明确同意。这一机制让库作者可以告知用户需要选择启用的特定条件,例如某个 API 处于实验状态、未来可能发生变化。
为了保护用户,编译器会就这些条件发出警告,并要求用户先选择启用,然后才能使用该 API。
选择启用 API
如果库作者把其库 API 中的某个声明标记为**要求选择启用**,那么你在代码中使用它之前必须明确表示同意。有几种选择启用的方式,我们建议选择最适合你情况的方式。
在局部选择启用
要在代码中使用某个特定 API 元素时选择启用,请使用 @OptIn 注解,并引用该实验性 API 的标记。例如,假设你想使用需要选择启用的 DateProvider 类:
1
2
3
4
5
6
7
8
9
| // 库代码
@RequiresOptIn(message = "This API is experimental. It could change in the future without notice.")
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class MyDateTime
@MyDateTime
// 需要选择启用的类
class DateProvider
|
在你的代码中,声明使用 DateProvider 类的函数之前,添加引用 MyDateTime 注解类的 @OptIn 注解:
1
2
3
4
5
6
7
8
| // 客户端代码
@OptIn(MyDateTime::class)
// 使用 DateProvider
fun getDate(): Date {
val dateProvider: DateProvider
// ...
}
|
需要注意的是,采用这种方式时,如果 getDate() 函数在你的代码中的其他地方被调用,或者被其他开发者使用,则不需要选择启用:
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 客户端代码
@OptIn(MyDateTime::class)
// 使用 DateProvider
fun getDate(): Date {
val dateProvider: DateProvider
// ...
}
fun displayDate() {
// 可以:不需要选择启用
println(getDate())
}
|
选择启用要求不会被传播,这意味着其他人可能在不知情的情况下使用实验性 API。为避免这种情况,更安全的做法是传播选择启用要求。
传播选择启用要求
当你在代码中使用供第三方使用的 API 时(例如在库中),你可以把它选择启用要求也传播到你的 API 上。为此,请用该库所使用的同一个**选择启用要求注解**标记你的声明。
例如,在声明使用 DateProvider 类的函数之前,添加 @MyDateTime 注解:
1
2
3
4
5
6
7
8
9
10
11
12
| // 客户端代码
@MyDateTime
fun getDate(): Date {
// 可以:该函数同样要求选择启用
val dateProvider: DateProvider
// ...
}
fun displayDate() {
println(getDate())
// 错误:getDate() 需要选择启用
}
|
如这个例子所示,被注解的函数看起来就像是 @MyDateTime API 的一部分。选择启用会把该选择启用要求传播给 getDate() 函数的使用者。
如果某个 API 元素的签名中包含需要选择启用的类型,那么该签名本身也必须要求选择启用。否则,如果某个 API 元素不要求选择启用,但其签名中包含需要选择启用的类型,使用它就会触发错误。
1
2
3
4
5
6
7
8
9
| // 客户端代码
@MyDateTime
fun getDate(dateProvider: DateProvider = DateProvider()): Date
@MyDateTime
fun displayDate() {
// 可以:该函数同样要求选择启用
println(getDate())
}
|
类似地,如果你对某个签名中包含需要选择启用类型的声明应用 @OptIn,选择启用要求仍然会被传播:
1
2
3
4
5
6
7
8
9
| // 客户端代码
@OptIn(MyDateTime::class)
// 由于签名中包含 DateProvider,会传播选择启用要求
fun getDate(dateProvider: DateProvider = DateProvider()): Date
fun displayDate() {
println(getDate())
// 错误:getDate() 需要选择启用
}
|
在传播选择启用要求时,有一点很重要:如果某个 API 元素变为稳定、不再有选择启用要求,那么其他仍然带有该选择启用要求的 API 元素仍保持实验性。例如,假设库作者因为 getDate() 函数现已稳定而移除了它的选择启用要求:
1
2
3
4
5
6
| // 库代码
// 不再要求选择启用
fun getDate(): Date {
val dateProvider: DateProvider
// ...
}
|
如果你使用 displayDate() 函数时没有移除该选择启用注解,那么即使已经不再需要选择启用,它仍然是实验性的:
1
2
3
4
5
6
7
8
| // 客户端代码
// 仍然是实验性的!
@MyDateTime
fun displayDate() {
// 使用稳定的库函数
println(getDate())
}
|
为多个 API 选择启用
要为多个 API 选择启用,请用它们所有的选择启用要求注解来标记该声明。例如:
1
2
| @ExperimentalCoroutinesApi
@FlowPreview
|
或者也可以使用 @OptIn:
1
| @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
在文件中选择启用
要对文件中的所有函数和类使用需要选择启用的 API,请在该文件的顶部、包声明和 import 之前添加文件级注解 @file:OptIn。
1
2
| // 客户端代码
@file:OptIn(MyDateTime::class)
|
在模块中选择启用
注意: -opt-in 编译器选项自 Kotlin 1.6.0 起可用。对于更早的 Kotlin 版本,请使用 -Xopt-in。
如果你不想为每次使用需要选择启用的 API 都添加注解,可以为整个模块选择启用。要为某个模块选择启用某个 API,请使用参数 -opt-in 编译它,并指定你所使用 API 的选择启用要求注解的完全限定名:-opt-in=org.mylibrary.OptInAnnotation。使用该参数编译的效果,等同于模块中的每个声明都带有注解 @OptIn(OptInAnnotation::class)。
如果你使用 Gradle 构建模块,可以这样添加参数:
Kotlin
1
2
3
4
5
6
| import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
// ...
tasks.named<KotlinCompilationTask<*>>("compileKotlin").configure {
compilerOptions.optIn.add("org.mylibrary.OptInAnnotation")
}
|
Groovy
1
2
3
4
5
6
7
8
| import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
// ...
tasks.named('compileKotlin', KotlinCompilationTask) {
compilerOptions {
optIn.add('org.mylibrary.OptInAnnotation')
}
}
|
如果你的 Gradle 模块是多平台模块,请使用 optIn 方法:
Kotlin
1
2
3
4
5
| kotlin {
compilerOptions {
optIn.add("org.mylibrary.OptInAnnotation")
}
}
|
Groovy
1
2
3
4
5
| kotlin {
compilerOptions {
optIn.add('org.mylibrary.OptInAnnotation')
}
}
|
对于 Maven,请使用以下配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| <build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>...</executions>
<configuration>
<args>
<arg>-opt-in=org.mylibrary.OptInAnnotation</arg>
</args>
</configuration>
</plugin>
</plugins>
</build>
|
要在模块级别为多个 API 选择启用,请为模块中使用的每个选择启用要求标记添加上述参数之一。
选择启用以便从类或接口继承
有时,库作者提供了某个 API,但希望要求用户明确选择启用后才能扩展它。例如,某个库 API 在使用上是稳定的,但在继承上不稳定,因为未来可能会用新的抽象函数来扩展它。库作者可以通过用 @SubclassOptInRequired 注解标记 open 或抽象类以及非函数式接口来强制这一点。
要选择启用以便使用这样的 API 元素并在代码中扩展它,请使用 @SubclassOptInRequired 注解并引用相应的注解类。例如,假设你想使用需要选择启用的 CoreLibraryApi 接口:
1
2
3
4
5
6
7
8
9
10
| // 库代码
@RequiresOptIn(
level = RequiresOptIn.Level.WARNING,
message = "Interfaces in this library are experimental"
)
annotation class UnstableApi()
@SubclassOptInRequired(UnstableApi::class)
// 需要选择启用才能扩展的接口
interface CoreLibraryApi
|
在你的代码中,创建继承自 CoreLibraryApi 接口的新接口之前,添加引用 UnstableApi 注解类的 @SubclassOptInRequired 注解:
1
2
3
| // 客户端代码
@SubclassOptInRequired(UnstableApi::class)
interface SomeImplementation : CoreLibraryApi
|
请注意,当你在类上使用 @SubclassOptInRequired 注解时,该选择启用要求不会传播到任何内部类或嵌套类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // 库代码
@RequiresOptIn
annotation class ExperimentalFeature
@SubclassOptInRequired(ExperimentalFeature::class)
open class FileSystem {
open class File
}
// 客户端代码
// 需要选择启用
class NetworkFileSystem : FileSystem()
// 嵌套类
// 不需要选择启用
class TextFile : FileSystem.File()
|
或者,你也可以使用 @OptIn 注解来选择启用。你还可以使用实验性标记注解,把该要求进一步传播到代码中对该类的任何使用处:
1
2
3
4
5
6
7
8
9
| // 客户端代码
// 使用 @OptIn 注解
@OptInRequired(UnstableApi::class)
interface SomeImplementation : CoreLibraryApi
// 使用引用注解类的注解
// 进一步传播选择启用要求
@UnstableApi
interface SomeImplementation : CoreLibraryApi
|
要求选择启用才能使用 API
你可以要求库的使用者先选择启用,然后才能使用你的 API。此外,在你决定移除选择启用要求之前,你可以告知使用者使用你的 API 的任何特殊条件。
创建选择启用要求注解
要要求选择启用才能使用你模块的 API,请创建一个注解类作为选择启用要求注解。该类必须使用 @RequiresOptIn 注解:
1
2
3
4
| @RequiresOptIn
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class MyDateTime
|
选择启用要求注解必须满足若干要求。它们必须具有:
BINARY 或 RUNTIME 保留策略。- 不能把
EXPRESSION、FILE、TYPE 或 TYPE_PARAMETER 作为目标。 - 没有参数。
选择启用要求可以有两种严重性级别之一:
RequiresOptIn.Level.ERROR。选择启用是强制性的。否则,使用被标记 API 的代码将无法编译。这是默认级别。RequiresOptIn.Level.WARNING。选择启用不是强制性的,但建议这样做。如果不选择启用,编译器会发出警告。
要设置所需的级别,请指定 @RequiresOptIn 注解的 level 参数。
此外,你还可以为 API 使用者提供一条 message。编译器会向试图在未选择启用的情况下使用该 API 的用户显示这条消息:
1
2
3
4
| @RequiresOptIn(level = RequiresOptIn.Level.WARNING, message = "This API is experimental. It can be incompatibly changed in the future.")
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class ExperimentalDateTime
|
如果你发布了多个相互独立、都需要选择启用的特性,请为每一个声明一个注解。这会让你的 API 对使用者更安全,因为他们只能使用自己明确接受的特性。这也意味着你可以独立地移除各特性的选择启用要求,从而使 API 更易于维护。
标记 API 元素
要要求选择启用才能使用某个 API 元素,请用选择启用要求注解标记它的声明:
1
2
3
4
5
| @MyDateTime
class DateProvider
@MyDateTime
fun getTime(): Time {}
|
请注意,对于某些语言元素,选择启用要求注解并不适用:
- 你不能为属性的幕后字段或 getter 添加注解,只能为属性本身添加。
- 你不能为局部变量或值参数添加注解。
要求选择启用才能扩展 API
有时你可能希望对 API 中哪些具体部分可以被使用和扩展拥有更细粒度的控制。例如,当你的某个 API 在使用上是稳定的,但:
- 在实现上不稳定,因为它仍在演进,例如你有一系列接口,并预期会添加没有默认实现的新抽象函数。
- 在实现上较为精细或脆弱,例如那些需要以协调方式运行的单个函数。
- 契约在未来可能被削弱,以对外的实现而言属于向后不兼容的方式,例如把输入参数
T 改为可空版本 T?,而之前的代码没有考虑 null 值。
在这种情况下,你可以要求用户先选择启用,然后才能进一步扩展你的 API。用户可以通过继承该 API 或实现抽象函数来扩展它。通过使用 @SubclassOptInRequired 注解,你可以对 open 或抽象类以及非函数式接口强制这一选择启用要求。
要为某个 API 元素添加选择启用要求,请使用 @SubclassOptInRequired 注解并引用相应的注解类:
1
2
3
4
5
6
7
8
9
| @RequiresOptIn(
level = RequiresOptIn.Level.WARNING,
message = "Interfaces in this library are experimental"
)
annotation class UnstableApi()
@SubclassOptInRequired(UnstableApi::class)
// 需要选择启用才能扩展的接口
interface CoreLibraryApi
|
请注意,当你使用 @SubclassOptInRequired 注解来要求选择启用时,该要求不会传播到任何内部类或嵌套类。
关于在你的 API 中如何使用 @SubclassOptInRequired 注解的真实示例,请查看 kotlinx.coroutines 库中的 SharedFlow 接口。
预稳定 API 的选择启用要求
如果你对尚未稳定的特性使用选择启用要求,请谨慎处理 API 的毕业过程,以免破坏客户端代码。
一旦你的预稳定 API 毕业并以稳定状态发布,请从声明中移除选择启用要求注解。这样客户端就可以不受限制地使用它们。不过,你应把注解类保留在模块中,以便现有客户端代码保持兼容。
为了鼓励 API 使用者从自己的代码中移除所有注解并重新编译以更新模块,请把这些注解标记为 @Deprecated,并在弃用消息中给出说明。
1
2
3
| @Deprecated("This opt-in requirement is not used anymore. Remove its usages from your code.")
@RequiresOptIn
annotation class ExperimentalDateTime
|