(译) Google Kotlin CodeStyle · Averson

社会主义新天地 提交于 2019-12-01 15:43:14

Google 官方 Kotlin 编码风格翻译

源文件

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

命名

如果源文件只包含一个顶级类(Top-level),文件名应该命名为大写小敏感和 .kt 拓展名。其他情况如果源文件包含多个顶级声明,则选择一个描述文件内容的名称,使用驼峰命名法,并附上名称和 .kt 拓展名。

Top-level 解释请参考该链接

12345678910
class  { }// Bar.kt class Bar { }fun Runnabel.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) 唯一能出现在源文件中的空格。

0x20 表示空格

这意味着:

  • 所有其他空格字符在字符串和字符源文本中均被转义
  • 制表符用于缩进

源文本(literal),是指程序源代码中用来表示固定的值的符号序列。例如在大多数语言中,引号包围的字符序列即为字符串源文本(string literal),表示一个特定的字符串值。

特殊的转义序列

对于任何具有特殊转义的字符(b、n、r、t、’、 和 $)使用的是对应的 Unicode 编码(例如:Line Feed : n = u000a)转义。

Non-ASCII 字符

对于剩下的 Non-ASCII 字符,要么使用实际的 Unicode 字符(例如:∞),要么使用等效的 Unicode 转义(如:u221e)。这个选择只取决于代码易读和理解。对于任何位置的可打印的字符,都不建议使用 Unicode 转义,尤其在字符串源文本和注释。

例子 建议
val unitAbbrev = "μs" 最好的:即使没有评论,也非常清楚
val unitAbbrev = "u03bcs" // μs" 较差的:没有理由在一个可打印字符字符中使用转义字符表示
val unitAbbrev = "u03bcs" 较差的:代码阅读者不知道这是一个什么概念
return "ufeff" + content 良好的:对不可打印字符使用转义,必要时可以进行注释。

结构

一个 .kt 文件由以下组成:

  1. 版权和许可证(可选)
  2. File-Level 注解
  3. 包名声明
  4. 导包声明
  5. Top-level 声明

每个部分均用换行来分隔。

版权 / 许可证

如果文件中包含版权或许可证,则应将其放在多行注释的最上面。

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

不要使用 KDoc-style 或者单行注释。

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

File-Level 注解

如果使用 kotlinFile-Level 注解请放在头部和包名声明之间。

1234567
/* *  Copyright 2017 Google Inc. */("ViewUtils")package domain.util

包名声明

包名声明不受行列限制并且 使用 line-wrap 模式。

line-wrap 参考维基百科定义

导包声明

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

不允许使用通配符导入。

和”包名声明”一样,导包声明不受行列限制并且 使用 line-wrap 模式。

Top-level 声明

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

文件中内容应该围绕一个主题。

比如只有一个公开类型(public)或一组拓展函数,对于多个接收者都应该执行同样的操作。

比如(AreaMaths.kt):

1234567
fun Circle.area() : Double {    // ...}fun Rectangle.area() : Double {    // ...}

不相关的声明应该分离开来放到自己的文件中,并且在一个文件内公开声明应该最小化尽可能的保持内聚。

没有文件内容的数量和排序限制。

源文件一般是从上至下阅读,所以尽可能按这个顺序反应出相关重要的内容。因此不同的文件可能会选择不同的排序。比如说,一个文件可能包含 100 个属性,10 个函数,1 个类。最重要的一点,每个类应该以某种逻辑去排序它的成员,维护者应该要能解释这种排序逻辑。再如:新函数不应该添加在类的末尾,因为这样是日期排序,这不是一个逻辑排序。

成员变量排序

类成员变量的排序和 Top-level 声明一致

格式

大括号

when 分支和不具有 if/else 的分支且适用于单行的语句不需要大括号。

12345
if (string.isEmpty()) returnwhen(value) {    () -> return}

其他情况都需要大括号,比如说 ifforwhendowhile。即使语句体是空语句或者只有一句也需要大括号。

123456
if (string.isEmpty())     return // WRONG!if (string.isEmpty()) {    return // OKay}

非空块

对于非空块和块状结构,大括号遵循 Kernighan(K) 和 Ritchie(R) 风格 (Egyptian brackets):

  • 左大括号前不换行
  • 左大括号后换行
  • 右大括号前换行
  • 如果右大括号是一个语句、函数体或类的终止,则右大括号后换行; 否则不换行。例如,如果右大括号后面是 else 或逗号,则不换行。
123456789101112131415161718192021
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 风格

12345678
try {    doSomething()} catch (e: Exception) {} // WRONG!!try {    doSomething()} catch (e: Exception) {} // OKay

表达式

只有在单行能够完成整个表达式的 if/else 语句才适用省略大括号。

123456789101112
val value = if (string.isEmpty()) 0 else 1 // OKayval value = if (string.isEmpty())  // WRONG!!                0            else                1val value = if (string.isEmpty()) { // OKay    0} else {    1}

缩进

每当开始一个新的块,缩进增加 4 个空格。

每行一个语句

每个语句后面都有一个换行符。不使用分号。

自动换行

一般情况下,一行长代码为了避免超出列限制(100个字符)而被分为多行,我们称之为自动换行(line-wrapping)。除了以下指出的情况,任何超出这些限制的必须按照说明换行。

  • 不可能遵循列限制的行(例如,KDoc中的一个长URL)
  • 包和导入语句
  • 在注释中可以剪切并粘贴到 shell 中的命令行。

从哪里断开

自动换行的基本准则是:更倾向于在更高的语法级别处断开。

  1. 如果在非赋值运算符处断开,那么在该符号前断开( +,它将位于下一行)。这条规则也适用以下类操作符语法:
    • 点分隔符(.)
    • 双冒号(::)
  2. 当赋值运算符在一行断开,符号后面会出现断开。
  3. 一个方法或者构造函数的参数名依然在左括号后面
  4. 逗号(,)与其前面的内容留在同一行。
  5. lambda 的 -> 符号和参数列表在同一行
123456789101112131415
val longString = "some long text"                  + "some more long text"fun longNameFunc(param1: string, param2: string                 param3: string, param4: string) {}Flowable    .fromCallable {}    .subscribeOn()    .map(DataMapper::toData)    .subscribe()val list: List<String> thisIsLongNamingVal    = listOf("")

注意:换行的主要目的是代码更清晰,而不一定是最小行数的代码。

继续缩进

当自动换行时,第一行后的每一行至少比第一行多缩进 4 个空格(注意:制表符不用于缩进)。当存在连续自动换行时,缩进可能会多缩进不止 4 个空格(语法元素存在多级时)。一般而言,两个连续行使用相同的缩进当且仅当它们开始于同级语法元素。

函数

当一个函数签名不适合一行时,将每个参数声明分解到它自己的行上。在这种格式中定义的参数应该使用继续缩进(+8)。闭括号())和返回类型被放在它们自己的行上,没有附加的缩进。

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

表达式函数

当一个函数只包含一个表达式时,它可以被表示为一个表达式函数。

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

替换为

1
override fun toString(): String = "Hey"

表达式函数不应该使用换行导致用两行表示。

如果表达式函数需要拓展则需要换行,使用正常的函数体、返回声明和正常的表达式换行规则。

属性

当一个属性初始化器不适用一行时,在等号(=)之后之换行并使用继续缩进规则。

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

属性声明一个 getter 或者 setter 函数应该在它们所在行前加一个普通缩进(+4)。
使用与函数相同规则进行格式化。

1234567
val directory: File? = null    set(value) {        // …    }    get() {        return value    }

对于只读属性可以使用更简洁的语法,适用于一行。

1
val defaultExtension: String get() = "kt"

空白

垂直方向

垂直方向空白行出现原则:

  • 类的连续成员: 属性、构造函数、函数、嵌套类等等。
    • 例外:
    • 两个连续属性之间的空行(在它们之间没有其他的代码)是可选的。
    • 如果存在,则使用这些空白行来创建属性的逻辑分组,并将属性与它们的相关属性关联起来。
  • 在函数体内,语句的逻辑分组间使用空行。
  • 类内的第一个成员变量前或最后一个成员变量后的空行是可选的(既不鼓励也不反对这样做,视个人喜好而定)。
  • 根据本文档的其他部分(如“结构”一节)的要求。

允许使用多个连续的空行,但不鼓励或不需要。

水平方向

除了语言需求和其它规则,并且除了文字,注释和 Kdoc 用到单个空格,单个 ASCII 空格也可以出现在以下几个地方:

  1. 分隔任何保留字与紧随其后的左括号(()(如:if、for 和 catch)。
1234567
// WRONG!for(i in 0..1) {}// OKayfor (i in 0..1) {}
  1. 分隔任何保留字与其前面的右大括号(}) (如:else, catch)。
1234567
// WRONG!}else {}// OKay} else {}
  1. 任何左大括号({)前
1234567
// WRONG!if (list.isEmpty()){}// OKayif (list.isEmpty()) {}
  1. 在任何二元或三元运算符的两侧
12345
// WRONG!val two = 1+1// OKayval two = 1 + 1

同样使用于 “类似操作符” 语法:

  • lambda 表达式(->
12345
// WRONG!ints.map { value->value.toString() }// OKayints.map { value -> value.toString() }

但是不适用于:

  • 双冒号(::)成员引用语法
12345
// WRONG! val toString = Any :: toString// OKayval toString = Any::toString
  • 逗号(.)
12345
// WRONG! it . toString()// OKayit.toString()
  1. 在一个冒号前使用(:)空格,仅在类使用基类或接口,或者用于 where 泛型约束时候。
12345
// WRONG!class : Runnable// Okayclass  : Runnable
12345
// WRONG!fun <T> max(a: T, b: T) where T: Comparable<T>// Okayfun <T> max(a: T, b: T) where T : Comparable<T>
  1. 逗号(,)或冒号(:)后。
1234567891011
// WRONG!val oneAndTwo = listOf(1,2)// Okayval oneAndTwo = listOf(1, 2)// WRONG!class  :Runnable// Okayclass  : Runnable
  1. 如果在一条语句后做注释,则双斜杠(//)两边都要空格。这里可以允许多个空格,但没有必要。
12345
// WRONG!var debugging = false//disabled by default// Okayvar debugging = false // disabled by default

注意:这个规则并不要求或禁止一行的开关或结尾需要额外的空格,只对内部空格做要求。

特殊结构

枚举类

一个没有函数的枚举,且它的常量没有文档,可以随意地格式化为单行。

1
enum class Answer { YES, NO, MAYBE }

当枚举中的常量被放在单独的行上时,它们之间不需要空白行,除非它们定义了一个正体。

12345678
enum class Answer {    YES,    NO,    MAYBE {        override fun toString() = """¯_(ツ)_/¯"""    }}

因为 enum 类是类,所以所有其他用于格式化类的规则都适用。

注解

在构造注释之前,成员或类型注释被放在单独的行上。

123
@Retention(SOURCE)@Target(FUNCTION, PROPERTY_SETTER, FIELD)annotation class Global

没有参数的注释可以放在一行上。

12
@JvmField @Volatilevar disposable: Disposable? = null

当只有一个没有参数的注释时,可以放在一行上。

12345
@Volatile var disposable: Disposable? = null@Test fun selectAll() {    // …}

隐式返回/属性类型

如果表达式函数体或属性初始化器是标量值,或者返回类型可以从正文中清楚地推断出来,那么就可以省略它。

123
override fun toString(): String = "Hey"// 转换为override fun toString() = "Hey"
123
private val ICON: Icon = IconLoader.getIcon("/icons/kotlin.png")// becomesprivate val ICON = IconLoader.getIcon("/icons/kotlin.png")

在编写库时,当它是公共 API 的一部分时,保留显式类型声明,方便框架使用者调用。

命名

识符只能使用 ASCII 字母和数字,因此每个有效的标识符名称都能匹配正则表达式 w+

Google 其它编程语言风格中使用的特殊前缀或后缀,如name_, mName, s_namekName,在 Java 编程风格中都不再使用。

特殊的前缀或后缀,如示例中所见的:name_,mNames_namekName,除了在备份属性(参见“支持属性”),这些都一律不使用。

包名

包名都是小写的,连续的单词简单地连接在一起(没有下划线)。

123456
// Okaypackage com.example.deepspace// WRONG!package com.example.deepSpace// WRONG!package com.example.deep_space

类名

类名是用使用驼峰命名,通常是名词或名词短语。例如:CharacterImmutableList。接口名也可能是名词或名词短语(例如:List),但我的有时是形容词或形容词短语(例如:Readable)。

函数名

函数名用使用驼峰命名,通常是动词或动词短语。例如:sendMessagestop。下划线允许出现在测试函数名中,以分离名称的逻辑组件。

123
@Test fun pop_emptyStack() {    // …}

常量命名

常量名命名模式为 CONSTANT_CASE,全部字母大写,用下划线分隔单词。那,到底什么算是一个常量?

常量是使用 val 定义并且没有 getter,其内容是不可更改的,其函数没有副作用。这包括不可变类型和不可变集合的不可变类型,如果它们标记为 const 则和标量和字符串一样。

12345
const val NUMBER = 5val NAMES = listOf("Alice", "Bob")val AGES = mapOf("Alice" to 35, "Bob" to 32)val COMMA_JOINER = Joiner.on(',') // Joiner is immutableval EMPTY_ARRAY = arrayOf<SomeMutableType>()

这些名字通常是名词或名词短语。常量值只能在一个 object class 定义或 top-level 中定义。否则,在类内部定义一个常量但定义的值必须使用一个非常量名。标量值的常量必须使用 const 修饰符。

12
const val HTTP_OK = 200class A {}
123
object A {    const val HTTP_OK = 200}

非常量命名

非常量的名称使用骆驼拼写法。应用于实例属性、本地属性和参数名。

1234567
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")

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

备份属性

当需要一个支持属性时,它的名字应该与真正的属性完全匹配,区别在于有一个下划线。

123456789
private var _table: Map<String, Int>? = nullval table: Map<String, Int>    get() {        if (_table == null) {            _table = HashMap()        }        return _table ?: throw AssertionError()    }

类型变量名

类型变量可用以下两种风格之一进行命名:

  • 单个的大写字母,后面可以跟一个数字(如:E, T, X, T2)。
  • 以类命名方式,后面加个大写的T(如:RequestT, FooBarT)。

驼峰式命名法(CamelCase)

驼峰式命名法分大驼峰式命名法(UpperCamelCase)和小驼峰式命名法(lowerCamelCase)。 有时,我们有不只一种合理的方式将一个英语词组转换成驼峰形式,如缩略语或不寻常的结构(例如”IPv6”或”iOS”)。Google指定了以下的转换方案。

名字从散文形式(prose form)开始:

  1. 把短语转换为纯ASCII码,并且移除任何单引号。例如:”Müller’s algorithm”将变成”Muellers algorithm”。
  2. 把这个结果切分成单词,在空格或其它标点符号(通常是连字符)处分割开。
    • 推荐:如果某个单词已经有了常用的驼峰表示形式,按它的组成将它分割开(如”AdWords”将分割成”ad words”)。 需要注意的是”iOS”并不是一个真正的驼峰表示形式,因此该推荐对它并不适用。
  3. 现在将所有字母都小写(包括缩写),然后将单词的第一个字母大写:
    • 每个单词的第一个字母都大写,来得到大驼峰式命名。
    • 除了第一个单词,每个单词的第一个字母都大写,来得到小驼峰式命名。
  4. 最后将所有的单词连接起来得到一个标识符。

示例:

Prose form 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 or YoutubeImporter*

加星号处表示可以,但不推荐。

注意:在英语中,某些带有连字符的单词形式不唯一。例如:”nonempty” 和 ”non-empty” 都是正确的,因此方法名 checkNonemptycheckNonEmpty 也都是正确的。

文档

文档格式

KDoc 块的基本格式如下所示:

1234567
/** * Multiple lines of KDoc text are written here, * wrapped normally… */fun method(arg: String) {    // …}

单行注释例子:

1
/** An especially short bit of KDoc. */

基本形式是可以接受的。

当整个 KDoc 块(包括注释标记)可以放在一行上时,可以替换单行表单。

请注意,只有在没有诸如 @return 这样的块标记时才会这样做。

段落

空行(即:只包含最左侧星号的行) - 出现在段落和段落标记组之前的标签。

块标签

标准 KDoc 出现顺序 @constructor, @receiver, @param, @property, @return, @throws, @see,出现这些都不会出现在空的描述。

当一个块标记不适合一行时,下一行从 @ 的位置缩进 8 个空格。

摘要片段

每个类或成员的 KDoc 以一个简短的摘要片段开始。这个片段是非常重要的,在某些情况下,它是唯一出现的文本,比如在类和方法索引中。

这只是一个小片段,可以是一个名词短语或动词短语,但不是一个完整的句子。它不会以 “A Foo is a…” 或 “This method returns…” 开头, 它也不会是一个完整的祈使句,如 “Save the record…”。然而,由于开头大写及被加了标点,它看起来就像是个完整的句子。

哪里需要使用 KDoc

至少在每个 public 类及它的每个 public 和 protected 成员变量处使用 KDoc,以下是一些例外:

  • 不言自明的方法,对于简单明显的方法如 getFoo,KDoc是可选的(即:是可以不写的)。这种情况下除了写 “Returns the foo”,确实也没有什么值得写了。
  • 如果有一些相关信息是需要读者了解的,那么以上的例外不应作为忽视这些信息的理由。例如,对于方法名 getCanonicalName,就不应该忽视文档说明,因为读者很可能不知道词语 canonical name 指的是什么。
  • 如果一个方法重写了超类中的方法,那么 KDoc 并非必需的。

参考

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!