三、编码规范
1、源代码组织
1)目录结构
在纯 Kotlin 项目中,推荐的目录结构遵循省略了公共根包的包结构。
对于JVM平台
2)源文件名称
文件名称使用首字母大写的驼峰风格(也称为 Pascal 风格)
文件的名称应该描述文件中代码的作用。因此,应避免在文件名中使用诸如“Util”之类的无意义词语。
3)源文件组织
不要只是为了保存 “Foo 的所有扩展函数”而创建文件。
4)类布局
不要按字母顺序或者可见性对方法声明排序,
不要将常规方法与扩展方法分开。
要把相关的东西放在一起,
选择一个顺序(高级别优先,或者相反)并坚持下去。
将嵌套类放在紧挨使用这些类的代码之后。如果打算在外部使用嵌套类,而且类中并没有引用这些类,那么把它们放到末尾,在伴生对象之后。
5)接口实现布局
在实现一个接口时,实现成员的顺序应该与该接口的成员顺序相同(如果需要, 还要插入用于实现的额外的私有方法)
6)重载布局
在类中总是将重载放在一起
2、命名规则
在 Kotlin 中,包名与类名的命名规则非常简单:
包的名称总是小写且不使用下划线(org.example.project)
类与对象的名称以大写字母开头并使用驼峰风格
1)函数名
函数、属性与局部变量的名称以小写字母开头、使用驼峰风格而不使用下划线
2)测试方法的名称
当且仅当在测试中,可以使用反引号括起来的带空格的方法名
class MyTestCase {
@Test fun `ensure everything works`() { /*...*/ }
@Test fun ensureEverythingWorks_onAndroid() { /*...*/ }
}
3)属性名
常量名称(标有 const 的属性,或者保存不可变数据的没有自定义 get 函数的顶层/对象 val 属性)应该使用大写、下划线分隔的名称
const val MAX_COUNT = 8
val USER_NAME_FIELD = "UserName"
保存带有行为的对象或者可变数据的顶层/对象属性的名称应该使用驼峰风格名称
val mutableCollection: MutableSet<String> = HashSet()
保存单例对象引用的属性的名称可以使用与 object 声明相同的命名风格
val PersonComparator: Comparator<Person> = /*...*/
对于枚举常量,可以使用大写、下划线分隔的名称,使用首字母大写的常规驼峰名称
3)幕后属性的名称
如果一个类有两个概念上相同的属性,一个是公共 API 的一部分,另一个是实现细节,那么使用下划线作为私有属性名称的前缀:
class C {
private val _elementList = mutableListOf<Element>()
val elementList: List<Element>
get() = _elementList
}
4)选择好名称
类的名称通常是用来解释类
方法的名称通常是动词或动词短语,说明该方法做什么
避免在名称中使用无意义的单词
当使用首字母缩写作为名称的一部分时,如果缩写由两个字母组成,就将其大写(IOStream); 而如果缩写更长一些,就只大写其首字母(XmlFormatter、 HttpInputStream)。
3、格式化
使用 4 个空格缩进。不要使用 tab。
对于花括号,将左花括号放在结构起始处的行尾,而将右花括号放在与左括结构横向对齐的单独一行
if (elements != null) {
for (element in elements) {
// ……
}
}
1)横向空白
(1)需要留空格的
在二元操作符左右留空格(a + b)
在控制流关键字(if、 when、 for 以及 while)与相应的左括号之间留空格。
在 // 之后留一个空格:// 这是一条注释
(2)不可以留空格的
不要在一元运算符左右留空格(a++)
不要在主构造函数声明、方法声明或者方法调用的左括号之前留空格。
绝不在 (、 [ 之后或者 ]、 ) 之前留空格
绝不在. 或者 ?.左右留空格:foo.bar().filter { it > 2 }.joinToString(), foo?.bar()
不要在用于指定类型参数的尖括号前后留空格:class Map<K, V> { …… }
不要在 :: 前后留空格:Foo::class、 String::length
不要在用于标记可空类型的 ? 前留空格:String?
2)冒号
(1)在 : 之后总要留一个空格。
(2)在以下场景中的 : 之前留一个空格
当它用于分隔类型与超类型时
当委托给一个超类的构造函数或者同一类的另一个构造函数时
在 object 关键字之后
abstract class Foo<out T : Any> : IFoo {
abstract fun foo(a: Int): T
}
class FooImpl : Foo() {
constructor(x: String) : this(x) { /*……*/ }
val x = object : IFoo { /*……*/ }
}
3)类头格式化
构造函数参数少于等于两个时,写一行
class Person(id: Int, name: String)
构造函数参数多于两个时,写一行。被继承的类,和右大括号,写在同一行
class Person(
id: Int,
name: String,
surname: String
) : Human(id, name) { /*……*/ }
对于多个接口,应该将超类构造函数(参数多的)调用放在首位,然后将每个接口应放在不同的行中:
class Person(
id: Int,
name: String,
surname: String
) : Human(id, name),
KotlinMaker { /*……*/ }
对于具有很长超类型列表的类,在冒号后面换行,并横向对齐所有超类型名:
class MyFavouriteVeryLongClassHolder :
MyLongHolder<MyFavouriteVeryLongClass>(),
SomeOtherInterface,
AndAnotherOne {
fun foo() { /*...*/ }
}
当类头很长时,可以在类头后放一空行 (如上例所示)或者将左花括号放在独立行上:
class MyFavouriteVeryLongClassHolder :
MyLongHolder<MyFavouriteVeryLongClass>(),
SomeOtherInterface,
AndAnotherOne
{
fun foo() { /*...*/ }
}
构造函数参数使用常规缩进(4 个空格)
4)修饰符
声明的多个修饰符,按顺序放
public / protected / private / internal
expect / actual
final / open / abstract / sealed / const
external
override
lateinit
tailrec
vararg
suspend
inner
enum / annotation / fun // 在 `fun interface` 中是修饰符
companion
inline
infix
operator
data
所有注解放在修饰符前:
@Named("Foo")
private val foo: Foo
除非编写库,否则省略多余的修饰符(例如 public)
5)注解格式化
注解在它们的声明之前,并使用相同的缩进:
@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude
无参数的注解放在同一行:
@JsonExclude @JvmField
var x: String
无参数的单个注解与相应的声明放在同一行
@Test fun foo() { /*……*/ }
6)文件注解
文件注解位于文件注释之后、package 语句之前,并且用一个空白行与 package 分开(为了强调其针对文件而不是包)
/** 授权许可、版权以及任何其他内容 */
@file:JvmName("FooBar")
package foo.bar
7)函数格式化
如果函数签名不适合单行,请这样写:
fun longMethodName(
argument: ArgumentType = defaultValue,
argument2: AnotherArgumentType,
): ReturnType {
// 函数体
}
函数参数使用常规缩进(4 个空格)
对于由单个表达式构成的函数体,优先使用表达式形式
fun foo(): Int { // 不良
return 1
}
fun foo() = 1 // 良好
8)表达式函数体格式化
如果函数的表达式函数体与函数声明不适合放在同一行,那么将 = 留在第一, 并将表达式函数体缩进 4 个空格。
fun f(x: String, y: String, z: String) =
veryLongFunctionCallWithManyWords(andLongParametersToo(), x, y, z)
9)属性格式化
非常简单的只读属性,用单行格式:
val isEmpty: Boolean get() = size == 0
复杂属性,总是将 get 与 set 关键字放在不同的行上:
val foo: String
get() { /*……*/ }
对于具有初始化器的属性,如果初始化器很长,那么在等号后增加一个换行并将初始化器缩进四个空格:
private val defaultCharset: Charset? =
EncodingRegistry.getInstance().getDefaultCharsetForPropertiesFiles(file)
10)格式化控制流语句
如果 if 或 when 语句的条件有多行,那么在语句体外边总是使用大括号。 将该条件的每个后续行相对于条件语句起始处缩进 4 个空格。 将该条件的右圆括号与左花括号放在单独一行
if (!component.isSyncing &&
!hasAnyKotlinRuntimeInScope(module)
) {
return createKotlinNotConfiguredPanel(module)
}
将 else、 catch、 finally 关键字以及 do/while 循环的 while 关键字与之前的花括号放在相同的行上。
关键字左右两边都是括号。
if (condition) {
// 主体
} else {
// else 部分
}
try {
// 主体
} finally {
// 清理
}
在 when 语句中,多个分支,用空行将其与相邻的分支块分开:
private fun parsePropertyValue(propName: String, token: Token) {
when (token) {
is Token.ValueToken ->
callback.visitValue(propName, token.value)
Token.LBRACE -> { // ……
}
}
}
将短分支放在与条件相同的行上,无需花括号。
when (foo) {
true -> bar() // 良好
false -> { baz() } // 不良
}
10)方法调用格式化
在较长参数列表的左括号后换行。按 4 个空格缩进参数。 将密切相关的多个参数分在同一行。和类头格式化相似
drawSquare(
x = 10, y = 10,
width = 100, height = 100,
fill = true
)
在分隔参数名与值的 = 左右留空格
11)链式调用换行
对链式调用换行,将 . 字符或者 ?. 操作符放在下一行,单倍缩进:
val anchor = owner
?.firstChild!!
.siblings(forward = true)
.dropWhile { it is PsiComment || it is PsiWhiteSpace }
调用链的第一个调用通常在换行之前
12)Lambda 表达式格式化
在 lambda 表达式中,花括号左右,分隔参数与代码体的箭头左右,留空格。 如果一个调用接受单个 lambda 表达式,应该尽可能将其放在圆括号外边传入。
list.filter { it > 10 }
为 lambda 表达式分配一个标签,不要在该标签与左花括号之间留空格:
fun foo() {
ints.forEach lit@{
// ……
}
}
在多行的 lambda 表达式中声明参数名时,将参数名放在第一行,后跟箭头与换行符:
appendCommaSeparated(properties) { prop ->
val propertyValue = prop.get(obj) // ……
}
如果参数列表太长而无法放在一行上,请将箭头放在单独一行:
foo {
context: Context,
environment: Env
->
context.configureEnv(environment)
}
4、使用尾随逗号
1)尾随逗号
尾随逗号是一系列元素最后一项之后的逗号符号:
它使版本控制差异更清晰 - 因为所有焦点都集中在更改的值上。
它使添加和重新排序元素变得容易——如果操作元素,则无需添加或删除逗号。
它简化了代码生成,例如,对象初始值设定项的代码生成。最后一个元素也可以有一个逗号。
尾随逗号是完全可选的 - 您的代码在没有它们的情况下仍然可以工作。
class Person(
val firstName: String,
val lastName: String,
val age: Int, // trailing comma
)
2)枚举
enum class Direction {
NORTH,
SOUTH,
WEST,
EAST, // trailing comma
}
3)值参数
fun shift(x: Int, y: Int) { /*...*/ }
shift(
25,
20, // trailing comma
)
val colors = listOf(
"red",
"green",
"blue", // trailing comma
)
4)类属性和参数
class Customer(
val name: String,
val lastName: String, // trailing comma
)
class Customer(
val name: String,
lastName: String, // trailing comma
)
5)函数值参数
fun powerOf(
number: Int,
exponent: Int, // trailing comma
) { /*...*/ }
constructor(
x: Comparable<Number>,
y: Iterable<Number>, // trailing comma
) {}
fun print(
vararg quantity: Int,
description: String, // trailing comma
) {}
6)具有可选类型的参数(包括setters)
val sum: (Int, Int, Int) -> Int = fun(
x,
y,
z, // trailing comma
): Int {
return x + y + x
}
println(sum(8, 8, 8))
7)索引后缀
class Surface {
operator fun get(x: Int, y: Int) = 2 * x + 4 * y - 10
}
fun getZValue(mySurface: Surface, xValue: Int, yValue: Int) =
mySurface[
xValue,
yValue, // trailing comma
]
8)Lambda 参数
fun main() {
val x = {
x: Comparable<Number>,
y: Iterable<Number>, // trailing comma
->
println("1")
}
println(x)
}
9)when入口
fun isReferenceApplicable(myReference: KClass<*>) = when (myReference) {
Comparable::class,
Iterable::class,
String::class, // trailing comma
-> true
else -> false
}
10)批注中的集合文本
annotation class ApplicableFor(val services: Array<String>)
@ApplicableFor([
"serializer",
"balancer",
"database",
"inMemoryCache", // trailing comma
])
fun run() {}
11)Type arguments实参
fun <T1, T2> foo() {}
fun main() {
foo<
Comparable<Number>,
Iterable<Number>, // trailing comma
>()
}
12)Type parameters形参
class MyMap<
MyKey,
MyValue, // trailing comma
> {}
13)解构声明
data class Car(val manufacturer: String, val model: String, val year: Int)
val myCar = Car("Tesla", "Y", 2019)
val (
manufacturer,
model,
year, // trailing comma
) = myCar
val cars = listOf<Car>()
fun printMeanValue() {
var meanValue: Int = 0
for ((
_,
_,
year, // trailing comma
) in cars) {
meanValue += year
}
println(meanValue/cars.size)
}
printMeanValue()
5、文档注释
对于较长的文档注释,将开头 /** 放在一个独立行中,并且后续每行都以星号开头:
/**
* 这是一条多行
* 文档注释。
*/
简短注释可以放在一行内:
/** 这是一条简短文档注释。 */
避免使用 @param 与 @return 标记。
将参数与返回值的描述直接合并到文档注释中,并在提到参数的任何地方加上参数链接。
只有当需要不适合放进主文本流程的冗长描述时才应使用 @param 与 @return。
// 避免这样:
/**
* Returns the absolute value of the given number.
* @param number The number to return the absolute value for.
* @return The absolute value.
*/
fun abs(number: Int) { /*……*/ }
// 而要这样:
/**
* Returns the absolute value of the given [number].
*/
fun abs(number: Int) { /*……*/ }
6、避免重复结构
不要在代码中保留不必要的语法元素 。
1)Unit
如果函数返回 Unit,那么省略返回类型:
fun foo() { // 这里省略了“: Unit”
}
2)分号
尽可能省略分号。
3)字符串模版
将简单变量传入到字符串模版中时不要使用花括号。只有用到更长表达式时才使用花括号。
println("$name has ${children.size} children")
7、语言特性的惯用法
1)不可变性
优先使用不可变(而不是可变)数据。初始化后未修改的局部变量与属性,总是将其声明为 val 而不是 var 。
使用不可变集合接口(Collection, List, Set, Map)来声明无需改变的集合。使用工厂函数创建集合实例时,尽可能选用返回不可变集合类型的函数:
// 不良:使用可变集合类型作为无需改变的值
fun validateValue(actualValue: String, allowedValues: HashSet<String>) { …… }
// 良好:使用不可变集合类型
fun validateValue(actualValue: String, allowedValues: Set<String>) { …… }
// 不良:arrayListOf() 返回 ArrayList<T>,这是一个可变集合类型
val allowedValues = arrayListOf("a", "b", "c")
// 良好:listOf() 返回 List<T>
val allowedValues = listOf("a", "b", "c")
2)默认参数值
优先声明带有默认参数的函数而不是声明重载函数。
// 不良
fun foo() = foo("a")
fun foo(a: String) { /*……*/ }
// 良好
fun foo(a: String = "a") { /*……*/ }
3)类型别名
如果有一个在代码库中多次用到的函数类型或者带有类型参数的类型,那么为它定义一个类型别名:
typealias MouseClickHandler = (Any, MouseEvent) -> Unit
typealias PersonIndex = Map<String, Person>
4)Lambda 表达式参数
在简短、非嵌套的 lambda 表达式中建议使用 it 用法而不是显式声明参数。
在有参数的嵌套 lambda 表达式中,始终应该显式声明参数。
5)在 lambda 表达式中返回
避免在 lambda 表达式中使用多个返回到标签。请考虑重新组织这样的 lambda 表达式使其只有单一退出点。
如果这无法做到或者不够清晰,请考虑将 lambda 表达式转换为匿名函数。
不要在 lambda 表达式的最后一条语句中使用返回到标签。
6)具名参数
当一个方法接受多个相同的原生类型参数或者多个 Boolean 类型参数时,使用具名参数语法,
除非在上下文中的所有参数的含义都已绝对清楚。
drawSquare(x = 10, y = 10, width = 100, height = 100, fill = true)
7)使用条件语句
优先使用 try、if 与 when 的表达形式。例如:
return if (x) foo() else bar()
return when(x) {
0 -> "zero"
else -> "nonzero"
}
优先选用上述代码而不是:
if (x)
return foo()
else
return bar()
when(x) {
0 -> return "zero"
else -> return "nonzero"
}
8)if 还是 when
二元条件优先使用 if 而不是 when。不要使用
when (x) {
null -> // ……
else -> // ……
}
而应使用 if (x == null) …… else ……
如果有三个或多个选项时优先使用 when
9)在条件中使用可空的 Boolean 值
如果需要在条件语句中用到可空的 Boolean, 使用 if (value == true) 或 if (value == false) 检测。
10)使用循环
优先使用高阶函数(filter、map 等)而不是循环。
例外:forEach(优先使用常规的 for 循环, 除非 forEach 的接收者是可空的或者 forEach 用做长调用链的一部分。)
当在使用多个高阶函数的复杂表达式与循环之间进行选择时,请了解每种情况下所执行操作的开销并且记得考虑性能因素
11)区间上循环
使用 until 函数在一个开区间上循环:
for (i in 0..n - 1) { /*……*/ } // 不良
for (i in 0 until n) { /*……*/ } // 良好
12)使用字符串
优先使用字符串模板而不是字符串拼接。
优先使用多行字符串而不是将 \n 转义序列嵌入到常规字符串字面值中。
如需在多行字符串中维护缩进,当生成的字符串不需要任何内部缩进时使用 trimIndent,而需要内部缩进时使用 trimMargin
assertEquals(
"""
Foo
Bar
""".trimIndent(),
value
)
val a = """if(a > 1) {
| return a
|}""".trimMargin()
13)函数还是属性
在某些情况下,不带参数的函数可与只读属性互换。
虽然语义相似,但是在某种程度上有一些风格上的约定。
底层算法优先使用属性而不是函数的情况:
(1)不会抛异常
(2)计算开销小(或者在首次运行时缓存)
(3)如果对象状态没有改变,那么多次调用都会返回相同结果
14)使用扩展函数
放手去用扩展函数。
每当你有一个主要用于某个对象的函数时,可以考虑使其成为一个以该对象为接收者的扩展函数。
为了尽量减少 API 污染,尽可能地限制扩展函数的可见性。根据需要,使用局部扩展函数、成员扩展函数或者具有私有可视性的顶层扩展函数。
15)使用中缀函数
一个函数只有用于两个角色类似的对象时才将其声明为中缀函数。良好示例如:and、 to、zip。 不良示例如:add。
如果一个方法会改动其接收者,那么不要声明为中缀形式
16)工厂函数
如果为一个类声明一个工厂函数,那么不要让它与类自身同名。优先使用独特的名称, 该名称能表明为何该工厂函数的行为与众不同。只有当确实没有特殊的语义时, 才可以使用与该类相同的名称。
例如:
class Point(val x: Double, val y: Double) {
companion object {
fun fromPolar(angle: Double, radius: Double) = Point(...)
}
}
如果一个对象有多个重载的构造函数,它们并非调用不同的超类构造函数,并且不能简化为具有默认参数值的单个构造函数,那么优先用工厂函数取代这些重载的构造函数。
17)平台类型
返回平台类型表达式的公有函数/方法必须显式声明其 Kotlin 类型:
fun apiCall(): String = MyJavaApi.getProperty("name")
任何使用平台类型表达式初始化的属性(包级别或类级别)必须显式声明其 Kotlin 类型:
class Person {
val name: String = MyJavaApi.getProperty("name")
}
使用平台类型表达式初始化的局部值可以有也可以没有类型声明:
fun main() {
val name = MyJavaApi.getProperty("name")
println(name)
}
18)使用作用域函数 apply/with/run/also/let
Kotlin 提供了一系列用来在给定对象上下文中执行代码块的函数:let、 run、 with、 apply 以及 also。
8、库的编码规范
在编写库时,建议遵循一组额外的规则以确保 API 的稳定性:
总是显式指定成员的可见性(以避免将声明意外暴露为公有 API )
总是显式指定函数返回类型以及属性类型(以避免当实现改变时意外更改返回类型)
为所有公有成员提供 KDoc 注释,不需要任何新文档的覆盖成员除外 (以支持为该库生成文档)