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