Kotlin 代码样式指南

在学习完语言语法后,要学好和用好一门开发语言,代码风格和单元测试是最基础的两个内容。其中单元测试在每个语言中基本相同,而代码风格或代码样式则需要以每个语言的推荐风格为标准,保持统一和规范使用,这在团队协作和开源项目中非常重要。

具体以方法或函数名命名来说例如 c 语言中使用 your_name 这种下划线和小写字母的命名方式,而 Android Java 则使用 yourName 这样的小驼峰式命名法。

代码样式是前行者在实践中总结出的优良写作风格,遵循代码样式规范,不仅是一种良好的编程习惯,更是一种认真的态度,是对前辈先行者和后进同业者的尊重。

不同的开发语言总是会有相应的推荐代码风格和单元测试方法,而这些在一般的教材书上却很少提及。我主要从事 Android 开发工作,常使用 Java 和 Kotlin 语言作为开发语言,遵循 Google 的 Android 开发代码样式规范。一般遵守 Android StudioIntellij Idea 内置的代码风格规范即可,也可以根据需要自定义 IDE 中的代码风格配置文件。通常使用快捷键 ( Linux 下为 CTR+ALT+L) 快速格式化代码风格。

本文主要介绍 Kotlin 相对于 Java 在书写样式上的不同,摘自官方文档,全部规范内容可以直接参考文后的 Kotlin 样式指南

大括号

when 分支不需要大括号,没有 else if/else 分支且适合放在一行上的 if 语句主体也不需要大括号。

    if (string.isEmpty()) return

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

除此以外,任何 ifforwhen 分支、dowhile 语句都需要大括号,即使主体为空或仅包含一个语句也是如此。

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

    if (string.isEmpty()) {
        return  // 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 中的长网址)
  • 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"

只有在表达式函数开始一个块时,才应换行。

    fun main() = runBlocking {
      // …
    }
    

否则,如果表达式函数增长到需要换行,应改用普通函数主体、return 声明和普通表达式换行规则。

属性

当属性初始化式不适合放在一行上时,应在等号 (=) 后面换行,并使用缩进。

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

声明 get 和/或 set 函数的属性应让每个函数独占一行,并使用正常的缩进 (+4)。对它们进行格式设置时,使用的规则与函数相同。

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

只读属性可以使用适合放在一行上的较短语法。

    val defaultExtension: String get() = "kt"

枚举类

对于没有函数且没有关于其常量的文档的枚举,可以选择性地将其格式设为单行。

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

隐式返回/属性类型

如果表达式函数主体或属性初始化式是标量值,或者可以根据主体明确推断出返回类型,则可以将其省略。

    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 大小写形式编写,通常是名词或名词短语。例如,CharacterImmutableList。接口名称也可以是名词或名词短语(例如 List),但有时还可以是形容词或形容词短语(例如 Readable)。

测试类的命名方式是以测试的类的名称开头且以 Test 结尾。例如,HashTestHashIntegrationTest

函数名称

函数名称采用 camelCase 大小写形式编写,通常是动词或动词短语。例如,sendMessagestop

允许在测试函数名称中出现下划线,用于分隔名称的逻辑组成部分。

    @Test fun pop_emptyStack() {
        // …
    }
    

常量名称

常量名称使用 UPPER_SNAKE_CASE 大小写形式:全部为大写字母,单词用下划线分隔。但究竟什么是常量呢?

常量是没有自定义 get 函数的 val 属性,其内容绝对不可变,并且其函数没有可检测到的副作用。这包括不可变类型和不可变类型的不可变集合,以及标量和字符串(如果标记为 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()
        }
    

更多关于注释、文档等内容基本和 Java 代码规范一致,请参考下面链接中的文档。

参考

Google Style Guides

Kotlin 样式指南

驼峰式大小写

匈牙利命名法