Kotlin 样式指南

本文档提供了 Google 针对 Kotlin 编程语言源代码的 Android 编码标准的完整定义。当且仅当 Kotlin 源文件遵循本文中的规则时,才称其为符合 Google Android 样式。

与其他编程样式指南一样,所涵盖的问题不仅包括格式的美学问题,还包括其他类型的约定或编码标准。然而,本文档主要关注我们普遍遵循的硬性规定,并避免提供无法明确执行(无论是人工还是工具)的建议。

最后更新:2023-09-06

源文件

所有源文件都必须使用 UTF-8 编码。

命名

如果源文件仅包含单个顶级类,则文件名应与大小写敏感的名称加上 .kt 扩展名一致。否则,如果源文件包含多个顶级声明,请选择一个描述文件内容且使用 PascalCase(如果文件名是复数形式,则 camelCase 也可以接受)的名称,并附加 .kt 扩展名。

// MyClass.kt
class MyClass { }
// Bar.kt
class Bar { }
fun Runnable.toBar(): Bar = // …
// Map.kt
fun <T, O> Set<T>.map(func: (T) -> O): List<O> = // …
fun <T, O> List<T>.map(func: (T) -> O): List<O> = // …
// extensions.kt
fun MyClass.process() = // …
fun MyResult.print() = // …

特殊字符

空白字符

除了行终止符序列,ASCII 水平空格字符 (0x20) 是源文件中唯一出现的空白字符。这意味着:

  • 字符串和字符字面量中的所有其他空白字符都经过转义。
  • 制表符用于缩进。

特殊转义序列

对于任何具有特殊转义序列的字符(\b\n\r\t\'\"\\\$),应使用该序列而非对应的 Unicode(例如 \u000a)转义。

非 ASCII 字符

对于剩余的非 ASCII 字符,可以使用实际的 Unicode 字符(例如 )或等效的 Unicode 转义(例如 \u221e)。选择取决于哪种方式使代码更易读和理解。不建议在任何位置对可打印字符使用 Unicode 转义,强烈不建议在字符串字面量和注释之外使用。

示例 讨论
val unitAbbrev = "μs" 最佳:即使没有注释也完全清晰。
val unitAbbrev = "\u03bcs" // μs 不佳:没有理由对可打印字符使用转义符。
val unitAbbrev = "\u03bcs" 不佳:读者不知道这是什么。
return "\ufeff" + content 良好:对不可打印字符使用转义,必要时添加注释。

结构

一个 .kt 文件按以下顺序包含内容:

  • 版权和/或许可证头(可选)
  • 文件级注解
  • 包声明
  • 导入声明
  • 顶级声明

每个部分之间恰好用一个空行隔开。

如果版权或许可证头部应包含在文件中,则应将其直接放在多行注释的顶部。

/*
 * Copyright 2017 Google, Inc.
 *
 * ...
 */
 

请勿使用 KDoc 样式或单行样式注释。

/**
 * Copyright 2017 Google, Inc.
 *
 * ...
 */
// Copyright 2017 Google, Inc.
//
// ...

文件级注解

带有“file”use-site target 的注解放置在任何头部注释和包声明之间。

包声明

包声明不受任何列宽限制,且从不换行。

导入声明

类、函数和属性的 import 语句合并在一个列表中并按 ASCII 排序。

不允许使用通配符导入(任何类型)。

与包声明类似,导入声明不受列宽限制,且从不换行。

顶级声明

一个 .kt 文件可以在顶层声明一个或多个类型、函数、属性或类型别名。

文件的内容应专注于一个主题。例如,一个公共类型或一组对多个接收器类型执行相同操作的扩展函数。不相关的声明应分离到各自的文件中,并且单个文件中的公共声明应最小化。

对文件内容的数量和顺序没有明确限制。

源文件通常从上到下阅读,这意味着顺序通常应反映出靠前的声明有助于理解靠后的声明。不同的文件可以选择不同的内容顺序。同样,一个文件可能包含 100 个属性,另一个包含 10 个函数,还有一个只包含一个类。

重要的是,每个文件都使用某种逻辑顺序,其维护者在被询问时可以解释这种顺序。例如,新的函数不会仅仅习惯性地添加到文件的末尾,因为那会导致“按添加日期时间顺序”排序,这并不是一种逻辑顺序。

类成员排序

类内成员的顺序遵循与顶级声明相同的规则。

格式

括号

对于没有超过一个 else 分支且可容纳在单行内的 when 分支和 if 表达式,不需要大括号。

if (string.isEmpty()) return

val result =
    if (string.isEmpty()) DEFAULT_VALUE else string

when (value) {
    0 -> return
    // …
}

否则,对于任何 ifforwhen 分支、dowhile 语句和表达式,即使主体为空或仅包含单个语句,也需要大括号。

if (string.isEmpty())
    return  // WRONG!

if (string.isEmpty()) {
    return  // Okay
}

if (string.isEmpty()) return  // WRONG
else doLotsOfProcessingOn(string, otherParametersHere)

if (string.isEmpty()) {
    return  // Okay
} else {
    doLotsOfProcessingOn(string, otherParametersHere)
}

非空代码块

非空代码块和类代码块遵循 Kernighan 和 Ritchie 风格(“埃及括号”)

  • 左大括号前没有换行。
  • 左大括号后换行。
  • 右大括号前换行。
  • 右大括号后换行,仅当该大括号终止一个语句或终止一个函数、构造函数或命名类的正文时。例如,如果大括号后面跟着 else 或逗号,则在大括号后换行。
return Runnable {
    while (condition()) {
        foo()
    }
}

return object : MyClass() {
    override fun foo() {
        if (condition()) {
            try {
                something()
            } catch (e: ProblemException) {
                recover()
            }
        } else if (otherCondition()) {
            somethingElse()
        } else {
            lastThing()
        }
    }
}

以下给出了一些枚举类的例外情况。

空代码块

空的块或类块必须采用 K&R 风格。

try {
    doSomething()
} catch (e: Exception) {} // WRONG!
try {
    doSomething()
} catch (e: Exception) {
} // Okay

表达式

用作表达式的 if/else 条件可以省略大括号,仅当整个表达式适合一行时。

val value = if (string.isEmpty()) 0 else 1  // Okay
val value = if (string.isEmpty())  // WRONG!
    0
else
    1
val value = if (string.isEmpty()) { // Okay
    0
} else {
    1
}

缩进

每当打开一个新的代码块或类代码块时,缩进增加四个空格。当代码块结束时,缩进返回到上一个缩进级别。缩进级别适用于整个代码块中的代码和注释。

每行一个语句

每个语句后面都有一个换行符。不使用分号。

行包装

代码的列宽限制为 100 个字符。除非另有说明,任何超出此限制的行都必须进行行包装,如下所述。

例外情况

  • 无法遵守列限制的行(例如,KDoc 中的长 URL)
  • packageimport 语句
  • 注释中可以剪切粘贴到 shell 中的命令行

在哪里断开

行包装的首要原则是:优先在更高的语法级别处断开。此外:

  • 当一行在运算符或中缀函数名处断开时,断点出现在运算符或中缀函数名之后。
  • 当一行在以下“类似运算符”符号处断开时,断点出现在符号之前:
    • 点分隔符(.?.)。
    • 成员引用中的两个冒号(::)。
  • 方法或构造函数名称与其后面的左括号(()保持连接。
  • 逗号(,)与其前面的标记保持连接。
  • Lambda 箭头(->)与其前面的参数列表保持连接。

函数

当函数签名不适合单行时,将每个参数声明拆分到单独的行。以这种格式定义的参数应使用单个缩进 (+4)。右括号())和返回类型放置在单独的行上,没有额外缩进。

fun <T> Iterable<T>.joinToString(
    separator: CharSequence = ", ",
    prefix: CharSequence = "",
    postfix: CharSequence = ""
): String {
    // …
}
表达式函数

当函数只包含一个表达式时,可以将其表示为表达式函数

override fun toString(): String {
    return "Hey"
}
override fun toString(): String = "Hey"

属性

当属性初始化器不适合单行时,在等号(=)后换行并使用缩进。

private val defaultCharset: Charset? =
    EncodingRegistry.getInstance().getDefaultCharsetForPropertiesFiles(file)

声明 get 和/或 set 函数的属性应将每个函数放置在单独的行上,并使用正常缩进 (+4)。使用与函数相同的规则进行格式化。

var directory: File? = null
    set(value) {
        // …
    }
只读属性可以使用更短的单行语法。
val defaultExtension: String get() = "kt"

空白

垂直

出现一个空行

  • 类连续成员之间:属性、构造函数、函数、嵌套类等。
    • 例外:两个连续属性(之间没有其他代码)之间的空行是可选的。根据需要使用此类空行来创建属性的逻辑分组,并将属性与其支持属性(如果存在)关联起来。
    • 例外:枚举常量之间的空行在下面涵盖。
  • 语句之间,根据需要将代码组织成逻辑子部分。
  • 函数中第一个语句之前、类中第一个成员之前或类中最后一个成员之后可选(既不鼓励也不反对)。
  • 根据本文档其他部分(例如 结构部分)的要求。

允许连续多个空行,但不鼓励或从不要求。

水平

除了语言或其他样式规则所要求,以及字面量、注释和 KDoc 之外,单个 ASCII 空格仅出现在以下位置:

  • 将任何保留字(例如 ifforcatch)与其后同一行上的左括号(()分开。
    // WRONG!
    for(i in 0..1) {
    }
    // Okay
    for (i in 0..1) {
    }
  • 将任何保留字(例如 elsecatch)与其前同一行上的右大括号(})分开。
    // WRONG!
    }else {
    }
    // Okay
    } else {
    }
  • 在任何左大括号({)之前。
    // WRONG!
    if (list.isEmpty()){
    }
    // Okay
    if (list.isEmpty()) {
    }
  • 在任何二元运算符的两侧。
    // WRONG!
    val two = 1+1
    // Okay
    val two = 1 + 1
    这也适用于以下“类似运算符”的符号:
    • lambda 表达式中的箭头(->)。
      // WRONG!
      ints.map { value->value.toString() }
      // Okay
      ints.map { value -> value.toString() }
    但不适用于:
    • 成员引用中的两个冒号(::)。
      // WRONG!
      val toString = Any :: toString
      // Okay
      val toString = Any::toString
    • 点分隔符(.)。
      // WRONG
      it . toString()
      // Okay
      it.toString()
    • 范围运算符(..)。
      // WRONG
      for (i in 1 .. 4) {
        print(i)
      }
      // Okay
      for (i in 1..4) {
        print(i)
      }
  • 仅当冒号(:)用于类声明中指定基类或接口,或用于 where 子句进行 泛型约束时,才在其之前添加空格。
    // WRONG!
    class Foo: Runnable
    // Okay
    class Foo : Runnable
    // WRONG
    fun <T: Comparable> max(a: T, b: T)
    // Okay
    fun <T : Comparable> max(a: T, b: T)
    // WRONG
    fun <T> max(a: T, b: T) where T: Comparable<T>
    // Okay
    fun <T> max(a: T, b: T) where T : Comparable<T>
  • 在逗号(,)或冒号(:)之后。
    // WRONG!
    val oneAndTwo = listOf(1,2)
    // Okay
    val oneAndTwo = listOf(1, 2)
    // WRONG!
    class Foo :Runnable
    // Okay
    class Foo : Runnable
  • 在双斜杠(//)两侧,双斜杠表示行尾注释的开始。此处允许有多个空格,但不强制要求。
    // WRONG!
    var debugging = false//disabled by default
    // Okay
    var debugging = false // disabled by default

此规则绝不解释为要求或禁止行首或行尾的额外空格;它仅涉及内部空格。

特定结构

枚举类

没有函数且常量没有文档的枚举可以可选地格式化为单行。

enum class Answer { YES, NO, MAYBE }

当枚举中的常量放置在不同的行上时,除非它们定义了主体,否则它们之间不需要空行。

enum class Answer {
    YES,
    NO,

    MAYBE {
        override fun toString() = """¯\_(ツ)_/¯"""
    }
}

由于枚举类是类,所有其他类的格式规则都适用。

注解

成员或类型注解应放在单独的行上,紧邻被注解的构造之前。

@Retention(SOURCE)
@Target(FUNCTION, PROPERTY_SETTER, FIELD)
annotation class Global

没有参数的注解可以放在一行上。

@JvmField @Volatile
var disposable: Disposable? = null

当只有一个没有参数的注解时,可以将其与声明放在同一行。

@Volatile var disposable: Disposable? = null

@Test fun selectAll() {
    // …
}

@[...] 语法只能与显式的使用点目标一起使用,并且只能用于将 2 个或更多没有参数的注解组合在一行上。

@field:[JvmStatic Volatile]
var disposable: Disposable? = null

隐式返回/属性类型

如果表达式函数体或属性初始化器是标量值,或者返回类型可以从函数体中清晰推断出来,则可以省略它。

override fun toString(): String = "Hey"
// becomes
override fun toString() = "Hey"
private val ICON: Icon = IconLoader.getIcon("/icons/kotlin.png")
// becomes
private val ICON = IconLoader.getIcon("/icons/kotlin.png")

在编写库时,如果显式类型声明是公共 API 的一部分,则应保留它。

命名

标识符仅使用 ASCII 字母和数字,在少数情况下(如下所述)使用下划线。因此,每个有效的标识符名称都与正则表达式 \w+ 匹配。

不使用特殊前缀或后缀,如 name_mNames_namekName 示例中所示,但支持属性的情况除外(参见支持属性)。

包名

包名全部小写,连续的单词简单地连接在一起(没有下划线)。

// Okay
package com.example.deepspace
// WRONG!
package com.example.deepSpace
// WRONG!
package com.example.deep_space

类型名称

类名以 PascalCase 编写,通常是名词或名词短语。例如,CharacterImmutableList。接口名称也可以是名词或名词短语(例如 List),但有时也可以是形容词或形容词短语(例如 Readable)。

测试类的命名以其所测试的类的名称开头,并以 Test 结尾。例如,HashTestHashIntegrationTest

函数名称

函数名采用 camelCase 编写,通常是动词或动词短语。例如,sendMessagestop

下划线允许出现在测试函数名称中,以分隔名称的逻辑组成部分。

@Test fun pop_emptyStack() {
    // …
}

@Composable 注解并返回 Unit 的函数,其名称应采用 PascalCase 并以名词形式命名,如同它们是类型一样。

@Composable
fun NameTag(name: String) {
    // …
}

函数名称不应包含空格,因为并非所有平台都支持此功能(特别是在 Android 中并非完全支持)。

// WRONG!
fun `test every possible case`() {}
// OK
fun testEveryPossibleCase() {}

常量名称

常量名使用 UPPER_SNAKE_CASE:所有字母大写,单词之间用下划线分隔。但是,常量究竟是什么?

常量是 val 属性,没有自定义 get 函数,其内容是深度不可变的,并且其函数没有可检测的副作用。这包括不可变类型和不可变类型的不可变集合,以及如果标记为 const 的标量和字符串。如果实例的任何可观察状态可以改变,则它不是常量。仅仅打算从不修改对象是不够的。

const val NUMBER = 5
val NAMES = listOf("Alice", "Bob")
val AGES = mapOf("Alice" to 35, "Bob" to 32)
val COMMA_JOINER = Joiner.on(',') // Joiner is immutable
val EMPTY_ARRAY = arrayOf()

这些名称通常是名词或名词短语。

常量值只能在 object 内部或作为顶级声明来定义。其他满足常量要求但在 class 内部定义的值必须使用非常量名称。

标量常量值必须使用 const 修饰符

非常量名称

非常量名称采用 camelCase 编写。这适用于实例属性、局部属性和参数名称。

val variable = "var"
val nonConstScalar = "non-const"
val mutableCollection: MutableSet = HashSet()
val mutableElements = listOf(mutableInstance)
val mutableValues = mapOf("Alice" to mutableInstance, "Bob" to mutableInstance2)
val logger = Logger.getLogger(MyClass::class.java.name)
val nonEmptyArray = arrayOf("these", "can", "change")

这些名称通常是名词或名词短语。

支持属性

当需要支持属性时,其名称应与实际属性的名称完全匹配,但前面加上下划线。

private var _table: Map<String, Int>? = null

val table: Map<String, Int>
    get() {
        if (_table == null) {
            _table = HashMap()
        }
        return _table ?: throw AssertionError()
    }

类型变量名

每个类型变量以两种样式之一命名:

  • 单个大写字母,可选后跟单个数字(例如 ETXT2
  • 采用类所用形式的名称,后跟大写字母 T(例如 RequestTFooBarT

驼峰命名法

有时,将英语短语转换为驼峰命名法有不止一种合理的方式,例如存在首字母缩略词或“IPv6”或“iOS”等不寻常结构时。为了提高可预测性,请使用以下方案。

从名称的散文形式开始:

  1. 将短语转换为纯 ASCII 并删除任何撇号。例如,“Müller’s algorithm”可能变为“Muellers algorithm”。
  2. 将结果按空格和任何剩余标点符号(通常是连字符)分割成单词。建议:如果任何单词在常用用法中已经具有约定俗成的驼峰式外观,则将其分割成其组成部分(例如,“AdWords”变为“ad words”)。请注意,像“iOS”这样的词本身并不是真正的驼峰式;它违背了任何约定,因此本建议不适用。
  3. 现在将所有内容(包括首字母缩略词)转换为小写,然后执行以下操作之一:
    • 将每个单词的第一个字符大写以生成 PascalCase。
    • 将除第一个单词外每个单词的第一个字符大写以生成 camelCase。
  4. 最后,将所有单词连接成一个标识符。

请注意,原始单词的大小写几乎完全被忽略。

散文形式 正确 不正确
"XML Http Request" XmlHttpRequest XMLHTTPRequest
"new customer ID" newCustomerId newCustomerID
"inner stopwatch" innerStopwatch innerStopWatch
"supports IPv6 on iOS" supportsIpv6OnIos supportsIPv6OnIOS
"YouTube importer" YouTubeImporter YoutubeImporter*

(* 可接受,但不推荐。)

文档

格式

KDoc 块的基本格式见此示例:

/**
 * Multiple lines of KDoc text are written here,
 * wrapped normally…
 */
fun method(arg: String) {
    // …
}

……或此单行示例:

/** An especially short bit of KDoc. */

基本形式总是可以接受的。当整个 KDoc 块(包括注释标记)可以容纳在单行时,可以替换为单行形式。请注意,这仅适用于没有块标签(如 @return)的情况。

段落

段落之间以及块标签组(如果存在)之前,用一个空行(即仅包含对齐前导星号(*)的行)隔开。

块标签

所有使用的标准“块标签”按 @constructor@receiver@param@property@return@throws@see 的顺序出现,并且它们从不带空描述。当块标签不适合单行时,续行从 @ 的位置缩进 4 个空格。

摘要片段

每个 KDoc 块都以简短的摘要片段开头。此片段非常重要:它是文本中在某些上下文(例如类和方法索引)中唯一显示的部分。

这是一个片段——一个名词短语或动词短语,而不是一个完整的句子。它不以“A `Foo` is a...”或“This method returns...”开头,也不需要形成一个完整的祈使句,如“Save the record.”。但是,该片段的开头字母要大写,并且像完整的句子一样进行标点。

用法

至少,每个 public 类型以及此类类型的所有 publicprotected 成员都必须有 KDoc,但以下少数情况除外。

例外:不言自明的函数

对于“简单、明显”的函数,如 getFoo 和属性如 foo,KDoc 是可选的,前提是确实没有任何其他值得说明的内容,只有“返回 foo”。

不宜引用此例外来为省略典型读者可能需要了解的相关信息辩护。例如,对于名为 getCanonicalName 的函数或名为 canonicalName 的属性,如果典型读者可能不知道“规范名称”一词的含义,请不要省略其文档(理由是它只会说 /** Returns the canonical name. */)!

例外:重写

重写超类型方法的函数不总是需要 KDoc。