本文档提供了 Google 针对 Kotlin 编程语言源代码的 Android 编码标准的完整定义。当且仅当 Kotlin 源文件遵循本文中的规则时,才称其为符合 Google Android 样式。
与其他编程样式指南一样,所涵盖的问题不仅包括格式的美学问题,还包括其他类型的约定或编码标准。然而,本文档主要关注我们普遍遵循的硬性规定,并避免提供无法明确执行(无论是人工还是工具)的建议。
源文件
所有源文件都必须使用 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 // … }
否则,对于任何 if
、for
、when
分支、do
和 while
语句和表达式,即使主体为空或仅包含单个语句,也需要大括号。
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)
package
和import
语句- 注释中可以剪切粘贴到 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 空格仅出现在以下位置:
- 将任何保留字(例如
if
、for
或catch
)与其后同一行上的左括号((
)分开。// WRONG! for(i in 0..1) { }
// Okay for (i in 0..1) { }
- 将任何保留字(例如
else
或catch
)与其前同一行上的右大括号(}
)分开。// 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) }
- lambda 表达式中的箭头(
- 仅当冒号(
:
)用于类声明中指定基类或接口,或用于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_
、mName
、s_name
和 kName
示例中所示,但支持属性的情况除外(参见支持属性)。
包名
包名全部小写,连续的单词简单地连接在一起(没有下划线)。
// Okay package com.example.deepspace // WRONG! package com.example.deepSpace // WRONG! package com.example.deep_space
类型名称
类名以 PascalCase 编写,通常是名词或名词短语。例如,Character
或 ImmutableList
。接口名称也可以是名词或名词短语(例如 List
),但有时也可以是形容词或形容词短语(例如 Readable
)。
测试类的命名以其所测试的类的名称开头,并以 Test
结尾。例如,HashTest
或 HashIntegrationTest
。
函数名称
函数名采用 camelCase 编写,通常是动词或动词短语。例如,sendMessage
或 stop
。
下划线允许出现在测试函数名称中,以分隔名称的逻辑组成部分。
@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() }
类型变量名
每个类型变量以两种样式之一命名:
- 单个大写字母,可选后跟单个数字(例如
E
、T
、X
、T2
) - 采用类所用形式的名称,后跟大写字母
T
(例如RequestT
、FooBarT
)
驼峰命名法
有时,将英语短语转换为驼峰命名法有不止一种合理的方式,例如存在首字母缩略词或“IPv6”或“iOS”等不寻常结构时。为了提高可预测性,请使用以下方案。
从名称的散文形式开始:
- 将短语转换为纯 ASCII 并删除任何撇号。例如,“Müller’s algorithm”可能变为“Muellers algorithm”。
- 将结果按空格和任何剩余标点符号(通常是连字符)分割成单词。建议:如果任何单词在常用用法中已经具有约定俗成的驼峰式外观,则将其分割成其组成部分(例如,“AdWords”变为“ad words”)。请注意,像“iOS”这样的词本身并不是真正的驼峰式;它违背了任何约定,因此本建议不适用。
- 现在将所有内容(包括首字母缩略词)转换为小写,然后执行以下操作之一:
- 将每个单词的第一个字符大写以生成 PascalCase。
- 将除第一个单词外每个单词的第一个字符大写以生成 camelCase。
- 最后,将所有单词连接成一个标识符。
请注意,原始单词的大小写几乎完全被忽略。
散文形式 | 正确 | 不正确 |
---|---|---|
"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
类型以及此类类型的所有 public
或 protected
成员都必须有 KDoc,但以下少数情况除外。
例外:不言自明的函数
对于“简单、明显”的函数,如 getFoo
和属性如 foo
,KDoc 是可选的,前提是确实没有任何其他值得说明的内容,只有“返回 foo”。
不宜引用此例外来为省略典型读者可能需要了解的相关信息辩护。例如,对于名为 getCanonicalName
的函数或名为 canonicalName
的属性,如果典型读者可能不知道“规范名称”一词的含义,请不要省略其文档(理由是它只会说 /** Returns the canonical name. */
)!
例外:重写
重写超类型方法的函数不总是需要 KDoc。