Kotlin 语言参考文档 中文版 Help

可读性

要创建一个具有可读性的 API, 比起仅仅是编写清晰的代码, 涉及更多的问题. 它需要考虑周全的设计, 能够简化集成和使用. 本章讨论如何增强 API 的可读性, 方法包括在构建你的库时考虑可组合能力, 利用特定领域专用语言(Domain-Specific Language, DSL) 实现简洁而且表现力强大的设置, 以及使用扩展函数和属性实现清晰而且易维护的代码.

优先使用明确的可组合能力

库通常会提供一些高级操作, 用于进行自定义. 例如, 某个操作可能允许使用者提供他们自己的数据结构, 网络通道, 计时器, 或生存周期观察器. 但是, 通过额外的函数参数引入这些自定义选项, 可能显著的增加 API 的复杂度.

除了添加更多参数用于自定义之外, 设计一个 API, 让不同的行为可以组合在一起, 这样的方式会更加有效. 例如, 在协程的 Flow API 中, bufferingconflation 都实现为单独的函数. 这些函数可以与更加基础的操作连接在一起, 例如 filtermap, 而不是每个基础操作接受参数来控制缓冲和合并.

另一个例子是 Jetpack Compose 中的 Modifiers API. 它允许 Composable 组件接受单个 Modifier 参数, 处理共通的自定义选项, 例如填充(padding), 大小, 以及背景颜色. 这种方案避免了每个 Composable 组件都需要为这些自定义接受单独的参数, 简化了 API, 减少了复杂度.

Box( modifier = Modifier .padding(10.dp) .onClick { println("Box clicked!") } .fillMaxWidth() .fillMaxHeight() .verticalScroll(rememberScrollState()) .horizontalScroll(rememberScrollState()) ) { // Box 的内容放在这里 }

使用 DSL

一个 Kotlin 库能够通过提供构建器 DSL, 显著的增强可读性. 使用 DSL 让你能够 以简洁的方式重复进行特定领域专用数据的声明. 例如, 考虑下面的示例, 它来自一个基于 Ktor 的服务器应用程序:

fun Application.module() { install(ContentNegotiation) { json(Json { prettyPrint = true isLenient = true }) } routing { post("/article") { call.respond<String>(HttpStatusCode.Created, ...) } get("/article/list") { call.respond<List<CreateArticle>>(...) } get("/article/{id}") { call.respond<Article>(...) } } }

这段代码设置一个应用程序, 安装 ContentNegotiation plugin, 配置为使用 Json 序列化, 并设置路由, 让应用程序应答多个 /article endpoint 上的请求.

关于创建 DSL 的详细介绍, 请参见 类型安全的构建器. 在创建库时, 以下几点值得注意:

  • DSL 中使用的函数是构建器函数, 这类函数接受一个带接受者的 Lambda 表达式作为最后参数. 这种设计允许调用这些函数时不使用括号, 让语法更加清晰. 作为参数传递的 Lambda 表达式可以用来配置正在创建的实体. 在上面的示例中, 传递给 routing 函数的 Lambda 表达式, 用来配置路由的细节.

  • 创建类的实例的工厂函数应该使用与它的返回类型相同的名称, 并以大写字母开头. 在上面的示例中, 你可以在创建 Json 实例的地方看到这种工厂函数. 这些函数也可以接受 Lambda 表达式参数进行配置. 更多详情请参见 编码规约.

  • 由于在编译期间, 在提供给构建器函数的 Lambda 表达式内部, 无法确定是否已经设置了必须的属性, 我们建议将必须的值作为函数参数来传递.

使用 DSL 构建对象不仅能够提高可读性, 还能够改善向后兼容性, 并简化文档过程. 例如, 以下面的函数为例:

fun Json(prettyPrint: Boolean, isLenient: Boolean): Json

这个函数可以代替 Json{} DSL 构建器. 但是, DSL 方案具有明显的优点:

  • DSL 构建器比这个函数更容易维持向后兼容性, 因为添加新的配置选项只需要添加新的属性 (或者在另其它示例中, 是添加新的函数), 这是向后兼容的变更, 而修改一个既有函数的参数列表则不是.

  • 它还使创建和维护文档更加容易. 你可以对每个属性在它的声明处分别编写文档, 而不是在同一个地方, 对一个函数的很多参数编写文档.

使用扩展函数和扩展属性

我们推荐使用 扩展函数和扩展属性 提高可读性.

类和接口应该定义定义类型的核心概念. 附加的功能和信息应该写成扩展函数和属性. 这样可以让代码的阅读者清楚的知道, 附加的功能可以在核心概念的基础上实现, 附加的信息可以通过类型中的数据计算得到.

例如, CharSequence 类型 (String 也实现这个接口) 只包含最基本的信息, 以及访问它的内容的最基本的操作符:

interface CharSequence { val length: Int operator fun get(index: Int): Char fun subSequence(startIndex: Int, endIndex: Int): CharSequence }

与字符串相关的共通功能通常定义为扩展函数, 这些函数都可以在类型的核心概念和基本 API 的基础上实现:

inline fun CharSequence.isEmpty(): Boolean = length == 0 inline fun CharSequence.isNotEmpty(): Boolean = length > 0 inline fun CharSequence.trimStart(predicate: (Char) -> Boolean): CharSequence { for (index in this.indices) if (!predicate(this[index])) return subSequence(index, length) return "" }

应该考虑将计算得到的属性和一般的方法声明为扩展. 默认情况下, 只有常规属性, 覆盖, 以及重载操作符, 才应该声明为成员.

不要使用 boolean 类型作为参数

考虑下面的函数:

fun doWork(optimizeForSpeed: Boolean) { ... }

如果在你的 API 中提供这个函数, 它可能会被这样调用:

doWork(true) doWork(optimizeForSpeed=true)

在第一个调用中, 无法推断出这个 boolean 参数代表什么, 除非你在开启了参数名称提示功能的 IDE 中阅读代码. 使用命名参数确实清楚的表示了参数的意图, 但没有办法强制你的使用者采用这样的编码风格. 因此, 为了提高可读性, 你的代码不应该使用 boolean 类型的参数.

另一种选择是, API 可以创建一个单独的函数, 专门执行由 boolean 参数控制的任务. 这个函数应该使用一个描述性的名称, 表明它的功能.

例如, Iterable 接口有下面的扩展:

fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> fun <T, R : Any> Iterable<T>.mapNotNull( transform: (T) -> R? ): List<R>

而不是单个的方法:

fun <T, R> Iterable<T>.map( includeNullResults: Boolean = true, transform: (T) -> R ): List<R>

另一种好的方案是使用 enum 类定义不同的操作模式. 如果存在多种操作模式, 或者你期望这些模式未来会随着时间的推移发生变化, 那么这种方案会很有用.

适当使用数字类型

Kotlin 定义了一组数字类型, 你可以使用它们作为你的 API 的一部分. 下面是正确使用数字类型的方法:

  • 使用 Int, LongDouble 类型作为算数类型. 它们表示执行计算的值.

  • 不要对非算数的实体使用算数类型. 例如, 如果你将 ID 表示为 Long, 你的使用者可能会假设 ID 是按顺序分配的, 并因此比较 ID. 这肯能导致不可靠的或无意义的结果, 或造成对具体实现的依赖, 而具体实现可能会在没有警告的情况下发生变更. 更好的策略是为 ID 的抽象定义一个专用的类. 你可以使用 内联的值类(Inline value class) 来构建这样的抽象, 而不会影响性能. 具体的例子请参见 Duration 类.

  • Byte, FloatShort 类型是内存布局类型. 它们用来限制存储值时使用的内存容量, 例如, 在缓存中存储数据时, 或通过网络传输数据时. 只有当底层数据可靠的适合这些类型, 并且不需要进行计算时, 才应该使用这些类型.

  • 应该使用无符号整数类型 UByte, UShort, UIntULong 来利用给定格式的全部正数值范围. 这些类型适合于需要的值超过有符号类型数值范围的情况, 或与原生库交互的的情况. 但是, 对于领域问题只需要 无符号整数 的情况, 不要使用这些类型.

下一步

在本向导的下一部分, 你将学习一致性.

进入下一部分

最终更新: 2024/12/17