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)** 是源文件中任何位置出现的唯一空格字符。这意味着:

  • 字符串和字符文字中的所有其他空格字符都已转义。
  • 制表符不用于缩进。

特殊转义序列

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

非 ASCII 字符

对于其余的非 ASCII 字符,使用实际的 Unicode 字符(例如)或等效的 Unicode 转义(例如\u221e)。选择仅取决于哪种方式使代码更易于阅读和理解。对于任何位置的可打印字符,都不建议使用 Unicode 转义,并且强烈建议不要在字符串文字和注释之外使用 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() {
    // …
}

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

@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的函数使用PascalCased命名为名词,就像它们是类型一样。

@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. 现在将所有内容(包括缩写词)都小写,然后执行以下操作之一
    • 将每个单词的首字母大写以产生帕斯卡命名法。
    • 将除第一个单词之外的每个单词的首字母大写以产生驼峰式命名法。
  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这样的属性,如果实际上除了“返回 foo”之外没有其他值得说的话,则 KDoc 是可选的。

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

例外:覆盖

覆盖超类型方法的方法并不总是包含 KDoc。