Kotlin 样式指南

本文档是 Google Android Kotlin 编程语言源代码编码标准的完整定义。当且仅当 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) 是源文件中唯一出现的空白字符。这意味着:

  • 字符串和字符字面量中的所有其他空白字符都必须进行转义。
  • 使用制表符(Tab)进行缩进。

特殊转义序列

对于任何具有特殊转义序列的字符(\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" 使用处目标 的注解应放置在任何头部注释和包声明之间。

包声明

包声明不受任何列限制,也不会进行换行。

导入声明

类、函数和属性的导入声明应合并为一个列表,并按 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 and 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() {
    // …
}

返回 Unit@Composable 注解函数应采用 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()
    }

类型变量名

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

  • 单个大写字母,后跟可选的单个数字(例如 E, T, X, T2
  • 以类所用的形式命名,后跟大写字母 T(例如 RequestT, FooBarT

驼峰命名法(Camel case)

有时将英语短语转换为驼峰命名法不止一种合理方式,例如当存在首字母缩略词或诸如“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),如果除了“Returns the foo”之外确实没有其他值得说的话,那么 KDoc 是可选的。

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

例外:重写(Overrides)

重写超类型方法的方法不一定始终存在 KDoc。