Kotlin 代码样式指南
在学习完语言语法后,要学好和用好一门开发语言,代码风格和单元测试是最基础的两个内容。其中单元测试在每个语言中基本相同,而代码风格或代码样式则需要以每个语言的推荐风格为标准,保持统一和规范使用,这在团队协作和开源项目中非常重要。
具体以方法或函数名命名来说例如 c 语言中使用 your_name
这种下划线和小写字母的命名方式,而 Android Java 则使用 yourName
这样的小驼峰式命名法。
代码样式是前行者在实践中总结出的优良写作风格,遵循代码样式规范,不仅是一种良好的编程习惯,更是一种认真的态度,是对前辈先行者和后进同业者的尊重。
不同的开发语言总是会有相应的推荐代码风格和单元测试方法,而这些在一般的教材书上却很少提及。我主要从事 Android 开发工作,常使用 Java 和 Kotlin 语言作为开发语言,遵循 Google 的 Android 开发代码样式规范。一般遵守 Android Studio
或 Intellij Idea
内置的代码风格规范即可,也可以根据需要自定义 IDE
中的代码风格配置文件。通常使用快捷键 ( Linux 下为 CTR+ALT+L
) 快速格式化代码风格。
本文主要介绍 Kotlin 相对于 Java 在书写样式上的不同,摘自官方文档,全部规范内容可以直接参考文后的 Kotlin 样式指南。
大括号
when
分支不需要大括号,没有 else if/else
分支且适合放在一行上的 if
语句主体也不需要大括号。
if (string.isEmpty()) return
when (value) {
0 -> return
// …
}
除此以外,任何 if
、for
、when
分支、do
和 while
语句都需要大括号,即使主体为空或仅包含一个语句也是如此。
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
大小写形式编写,通常是名词或名词短语。例如,Character
或 ImmutableList
。接口名称也可以是名词或名词短语(例如 List
),但有时还可以是形容词或形容词短语(例如 Readable
)。
测试类的命名方式是以测试的类的名称开头且以 Test
结尾。例如,HashTest
或 HashIntegrationTest
。
函数名称
函数名称采用 camelCase
大小写形式编写,通常是动词或动词短语。例如,sendMessage
或 stop
。
允许在测试函数名称中出现下划线,用于分隔名称的逻辑组成部分。
@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 代码规范一致,请参考下面链接中的文档。