本文承载了 Google 对于 Android 上关于 Kotlin 编程语言的编码准则的完整定义。一份 Kotlin 源文件,当且仅当它遵守本文的规则书写时,可被称为符合了 Google Android Style。

像别的编程语言风格指南一样,所涉及的不仅仅是美化代码格式的问题,还包括其它类型的约定或者编码标准。然而,本文主要关注的还是我们应普遍遵循的硬性规定,并尽量避免给出不明确的强制性建议(无论是人或者工具)。

上次更新于: 2017-11-14

源文件

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

命名

如果一个源文件只包含一个顶级类,这个文件必须用能体现出类名且区分大小写的名称命名,并加上 .kt 扩展名。

如果包含多个顶级声明,请选择一个能够描述文件内容的名称,并用PascalCase格式(注:首字母大写,每个单词首字母大写,其余小写)表示,加上 .kt 扩展名。

// Foo.kt
class Foo { }

// 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> = // …

特殊字符

空白字符

除了行终止符外,ASCII 水平空格字符 (0x20) 是唯一允许出现在源文件的空白字符。这意为着:

  1. 字符串和字符字面量中所有其它空白字符都会被转义;
  2. 不要用制表符(TAB)来缩进。

特殊转义字符

特殊转义序列 (\b, \n, \r, \t, \', \", \\, 和 \$) 中的字符可以直接用,而不需要用相应的 Unicode (如\u000a) 来转义

非 ASCII 字符

对于非 ASCII 字符,用实际的 Unicode 字符(如)或者相同意义的 Unicode 转义 (如\u221e)都可以。根据较容易被阅读和理解的原则来选择其中一种。Unicode 转义不建议用于可打印字符,并且强烈反对在字符串字面量和注释之外使用。

示例 说明
val unitAbbrev = "μs" 完美: 即便没有注释也一目了然
val unitAbbrev = "\u03bcs" // μs 凑合: 可打印字符没有道理要用 Unicode 转义表示
val unitAbbrev = "\u03bcs" 不好: 读者完全看不懂这是个啥
return "\ufeff" + content 还行: 转义不可打印字符,并且要加上必要注释

结构

一个 .kt 文件依次由下列内容组成:

  1. Copyright / License 头 (可选)
  2. 文件级注解
  3. 包定义语句
  4. Import 语句
  5. 顶级声明(Top-level)

上述内容均用空行分隔。

如果有版权 copyright 或者授权 license 声明,必须放在顶部开始位置,用多行注释表示

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

不要用 KDoc 样式 或者单行注释.

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

文件级注解

使用目标点为 ‘file’ 的注解需要放到头部注释和包声明之间。

包声明语句

包声明不被行字数限制且不允许换行。

Import 语句

导入(import)类、函数和属性的语句,按类型分组并按 ASCII 码表序分别排列在一起。

不允许使用通配符(*)。

跟包声明一样不受行字数限制,不换行。

顶级声明

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

文件的内容应该聚焦在一个主题上,比如单个的公开(public)的类,或者一组在多个接收器类型上执行同样的操作的扩展函数。 不相关的声明要分开到各自的文件里,并应尽量的减少单个文件里的公开(public)声明。

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

阅读源代码通常是从上往下的,这意味着,在一般情况下,顺序上应该能体现出声明越靠上越能让他人对其理解得更为深入。 不同文件可能会按其内容选择不同的顺序书写。同样的,一个文件可能会包含100个属性,还有10个函数,甚至会有一个类。

重要的是,每个类应该有其一定逻辑上的顺序,维护者被问到时是可以解释其合理性的。例如,新增的函数不应只是习惯性的放到文件末尾,因为那样不过是迁就于“按日期时间顺序添加”的顺序,而不是一个合理的顺序。

类成员的顺序

类成员顺序跟顶级声明遵循同样的规则。

格式

大括号({})

when 的分支语句,以及 if 的语句没有 else if/else 分支且单行表示的,可以省略大括号。

if (string.isEmpty()) return

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

任何 ifforwhen的分支、do 以及 while 的语句,即使它们后面内容是空的或只有一行,都必须用大括号。

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

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

非空代码块(block)

非空代码块的大括号遵循 Kernighan & Ritchie 风格 (“埃及括号 Egyptian brackets”):

  • 左大括号前不换行
  • 左大括号后换行
  • 右大括号前换行
  • 右大括号后换行, 适用于以括号结束的语句或者函数、构造器、和类的结束。例如,大括号后跟着 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()
        }
    }
}

针对枚举类的特别情况在后文有提到。

空代码块(block)

空代码块需遵循 K&R 风格.

try {
    doSomething()
} catch (e: Exception) {} // 错了!
try {
    doSomething()
} catch (e: Exception) {
} // 可以

表达式

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中执行)

换行位置

换行的根本准则是:优先在较高的句法层面(syntactic level)上换行。

  1. 非赋值 运算符处换行时,在符号前换行。
    • 这也适用于”类运算符”的符号:
      • 点分隔符 (.)
      • 两个冒号的成员引用 (::)
  2. 赋值 运算符处换行时,在符号后换行。
  3. 在方法或者构造器的左括号 (() 之后换行
  4. 在跟在标记的逗号 (,) 之后换行
  5. 在跟在 lambda 的参数后面的箭头 (->) 之后换行

注意:换行的主要目的是让代码清晰,而不是过分追求更少的代码行数。

连续缩进

当换行后,每一行(每个连续行),相对原行至少要有 +8 个字符的缩进。

当有多个连续的换行后,缩进可能根据需要会增加到 +8 以上。 通常来说,当且仅当两个连续的缩进行在语法上为并行的元素开头,使用相同的缩进层级。

函数

当函数的签名一行容不下,在每个参数声明处都换行。这种格式下定义的参数应使用一个缩进(+4)。 闭合括号()) 要和返回类型放在同一行,且不用额外的缩进。

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

表达式函数

当一个函数只包含了一个表达式语句时,可以将其用表达式函数来表示。

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

表达式函数不应该被分成两行,如果一个表达式函数过长,需要被分行书写,这时应该用普通的函数体,return声明,符合换行规则的普通表达式来替代。

属性

当一个属性的初始化代码一行放不下时,可以在等号=之后换行,并使用连续缩进。

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

属性的setget函数应该放到它们各自的行,并用一个普通缩进(+4)。它们跟别的普通函数遵循一样的格式。

var directory: File? = null
    set(value) {
        // …
    }

只读属性若在一行内可以用更简短的语法表示。

val defaultExtension: String get() = "kt"

空行

纵向

空行出现在:

  1. 在类的连续成员之间:属性、构造器、函数、嵌套类等等
    • 例外: 两个连续的(没有其它代码)属性之间空行不是必须的。空行可以用于将属性按逻辑分组,还有将属性和其幕后属性(如果存在)作关联用。
    • 例外: 枚举常量之间的空白行下面有提到。
  2. 在语句之间,按需将代码组织成逻辑上的小节。
  3. 可选项:在函数的第一个语句之前,在类的第一个成员之前,或者类的最后一个成员之后(不鼓励也不反对)。
  4. 按本文其它章节的要求(如“结构”

允许多个连续的空行,但不鼓励也不强求。

横向

除了语言和其它样式规范的要求之外,不包括字面值、注释和KDoc,单个 ASCII 的空格仅可出现在下列位置:

  1. 隔开保留词(如ifforcatch)和跟在其后的开括号(()。
     // WRONG!
     for(i in 0..1) {
     }
    
     // Okay
     for (i in 0..1) {
     }
    
  2. 隔开保留词(如elsecatch)和跟在其后的右大括号(})。
     // WRONG!
     }else {
     }
    
     // Okay
     } else {
     }
    
  3. 在开大括号({)之前。
     // WRONG!
     if (list.isEmpty()){
     }
    
     // Okay
     if (list.isEmpty()) {
     }
    
  4. 在二元运算符两边。
     // 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)
      
  5. 冒号(:)之前,仅限于类声明中指定基类或接口的冒号,或者在泛型约束的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>
    
  6. 逗号(,)和冒号(:)之后。

     // WRONG!
     val oneAndTwo = listOf(1,2)
    
     // Okay
     val oneAndTwo = listOf(1, 2)
    
     // WRONG!
     class Foo :Runnable
    
     // Okay
     class Foo : Runnable
    
  7. 在行尾注释双斜杠(//)的两边。这里允许有多个空格,但不强制

     // 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() {
    // …
}

隐式返回/属性类型

若一个表达式函数或者属性的初始化代码,是标量值(如一个String),或者它们的返回值类型可以从后面的代码中推断出来, 那么类型可以省略。

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

函数名

函数名使用驼峰法命名,通常使用动词及动词短语,如sendMessagestop

测试函数名称中允许用下划线分隔其逻辑元素。

@Test fun pop_emptyStack() {
    // …
}

常量名

常量名用全大写的蛇形写法(UPPER_SNAKE_CASE):所有字母大写、单词之间用下划线隔开。

但究竟什么是常量?

常量是那些没有自定义getterval属性,其内容完全不可变,其函数没有可察觉的副作用(译者注:不会对函数外的任何东西作修改)。这包括不可变类型、不可变类型的不可变集合,以及标记为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 是不可变的
val EMPTY_ARRAY = arrayOf<SomeMutableType>()

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

常量只能定义在在一个对象(object)内或者作为一个顶级声明。否则,即使满足上述对常量的要求,定义在类(class)中的必须使用非常量的名称。

标量值类型(scalar values)的常量,必须用const修饰符

非常量命名

非常量的名字可采用驼峰写法。适用于实例的属性、局部属性和参数名。

val variable = "var"
val nonConstScalar = "non-const"
val mutableCollection: MutableSet<String> = 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()
    }

类型变量的名称

类型变量(注:用于泛型)按下面两种风格之一命名:

  1. 一个大写字母,可视情况跟个数字(如:ETXT2
  2. 用类型的名字加大写字母T(如:RequestT,FooBarT)

关于驼峰写法

有时候,会有不止一种合理的方式将英文短语转成驼峰风格,如缩略词或者类似于 “IPv6”、”iOS”等特别的结构。为了提高可预见性,可遵循下列方案。

从名字的原始形式开始:

  1. 将短语转为纯 ASCII 表示,并删除所有撇号。举个例子,”Müller’s algorithm” 可转成 “Muellers algorithm”。

  2. 将结果再分成单词,用空格和剩下的标点符号(一般是连接字符)分割。

    • 建议: 如果一个单词已经采用了驼峰写法,则按其组成部分分开(如 “AdWords” 转换成 “ad words”)。注意,像”iOS”这样的单词 本身 并不是真正的驼峰写法,它违背了所有惯例,因此这个建议对它不适用。
  3. 将所有字符都小写(包括缩略词),然后只将下面单词的首字符大写:

    • Pascal写法中单词,或者

    • 驼峰写法中的单词(第一个除外)

  4. 最后把所有的词组合成一个识别符。

需要注意的是,原始词组的外表几乎完全被忽略了。

原始形式 Correct Incorrect
“XML Http Request” XmlHttpRequest XMLHTTPRequest
“new customer ID” newCustomerId newCustomerID
“inner stopwatch” innerStopwatch innerStopWatch
“supports IPv6 on iOS” supportsIpv6OnIos supportsIPv6OnIOS
“YouTube importer” YouTubeImporter
YoutubeImporter*
 

注意:有些词组的连字符在英语中是可有可无的,例如,”nonempty”和”non-empty”都是可以的,所以方法名checkNonemptycheckNonEmpty同样都可以。

文档

格式

如下是基本的 KDoc 文档:

/**
 * KDoc 的多行文本可以书写到这里,
 * 普通的换行…
 */
fun method(arg: String) {
    // …
}

或者像这样的单行:

/** 一个特别短的 KDoc。 */

基本样式在任何情况下都是可以用的。单行样式可以用来替换一行就能整个放下的 KDoc(包括注释标记),需要注意这仅适用于没有块标签(如@return)的 KDoc。

段落

空行,即只有对齐用的前置星号(*)的行,可以出现在段落间,也可以出现在块标签组(如果有的话)之前

块标签

所有标准的“块标签”都要按@constructor@receiver@param@property@return@throws@see的顺序排列,而且标签的描述不能留白。当块标签一行放不下时,换行后的连续行从@的位置缩进8个空格。

摘要片段

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

一个片段是名词短语或动词短语,而非一个完整的句子。它不以“某某是一个……” 或者“这个方法返回……”开头,也不必是一个完整的祈使句,如“Save the record.”。虽然,片段用大写字母开头又有标点符号(指用英文书写的片段),看起来像是一个完整的句子。

用法

至少在每个 public 的类型,以及每个publicprotected 的成员上都要有 KDoc,下面的例子除外。

例外:自明(self-explanatory)函数

KDoc 对于“简单、明了”的函数(如getFoo)和属性(如foo)不是必需的,实在没什么值得说的时候就用“Returns the foo”吧。

举这个例子来说明其实不是很恰当,因为这样可能会省略掉某些读者正需要了解的相关信息。例如,函数名为getCanonicalName,或者属性名为canonicalName,如果某些读者可能不知道术语”canonical name”的意思的话,就不要省略它的文档(尽管术语解释起来可能只是一句/** Returns the canonical name. */)。

例外:覆写(override)

那些覆写自父类的方法并不是都会有 KDoc。