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” 使用站点目标 的注释放在任何头注释和包声明之间。

包语句

包语句不受任何列限制,并且永远不会换行。

导入语句

用于类、函数和属性的导入语句在一个列表中分组,并按 ASCII 排序。

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

与包语句类似,导入语句不受列限制,并且永远不会换行。

顶级声明

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

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

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

源文件通常从上到下读取,这意味着顺序通常应反映出上面的声明将有助于理解下面的声明。不同的文件可能会选择以不同的方式排序其内容。类似地,一个文件可能包含 100 个属性,另一个文件包含 10 个函数,而另一个文件可能只包含一个类。

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

类成员排序

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

格式

大括号

对于when分支和if表达式,如果它们不超过一个else分支并且适合在一行上,则不需要大括号。

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? = null

val table: Map
    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. 现在将所有内容都小写(包括首字母缩略词),然后执行以下操作之一
    • 将每个单词的第一个字符大写以生成帕斯卡大小写。
    • 将每个单词的第一个字符大写,除了第一个单词,以生成驼峰式大小写。
  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。