5.2 编码规范

原文链接: https://kotlinlang.org/docs/coding-conventions.html

5.2 编码规范

广为人知且易于遵循的编码规范对任何编程语言都至关重要。这里我们为使用 Kotlin 的项目提供代码风格和代码组织方面的指导。

在 IDE 中配置风格

Kotlin 最流行的两个 IDE —— IntelliJ IDEA 和 Android Studio —— 都为代码风格提供了强大的支持。你可以配置它们,按照给定的代码风格自动格式化代码。

应用风格指南

  1. 打开 Settings/Preferences | Editor | Code Style | Kotlin。
  2. 点击 Set from…。
  3. 选择 Kotlin style guide。

验证代码是否符合风格指南

  1. 打开 Settings/Preferences | Editor | Inspections | General。
  2. 启用 Incorrect formatting 检查。用于验证风格指南中其他问题(例如命名规范)的附加检查默认已启用。

更多信息请参阅用 IntelliJ IDEA 迁移到 Kotlin 代码风格指南。

源码组织

目录结构

在纯 Kotlin 项目中,推荐的目录结构遵循包结构,并省略公共的根包。例如,如果项目中的所有代码都位于 org.example.kotlin 包及其子包中,那么声明 org.example.kotlin 包的文件应直接放在源码根目录下,而 org.example.kotlin.network.socket 包中的文件应放在源码根目录的 network/socket 子目录中。

在 JVM 上:在 Kotlin 与 Java 混用的项目中,Kotlin 源文件应与 Java 源文件放在同一个源码根目录下,并遵循相同的目录结构:每个文件都应存储在与其 package 声明相对应的目录中。

源文件命名

如果某个 Kotlin 文件只包含单个类或接口(可能还有一些相关的顶层声明),它的名称应与类名相同,并加上 .kt 扩展名。这适用于所有类型的类和接口。如果一个文件包含多个类,或者只包含顶层声明,请选择一个能描述文件内容的名称,并据此命名文件。请使用大驼峰命名法,即每个单词的首字母大写。例如 ProcessDeclarations.kt。

文件名应描述文件中的代码做什么。因此,应避免在文件名中使用 Util 之类的无意义词汇。

多平台项目

在多平台项目中,位于平台特有源集内、包含顶层声明的文件应带有与该源集名称相关的后缀。例如:

  • jvmMain/kotlin/Platform.jvm.kt
  • androidMain/kotlin/Platform.android.kt
  • iosMain/kotlin/Platform.ios.kt

至于公共源集,包含顶层声明的文件不应带后缀。例如 commonMain/kotlin/Platform.kt。

技术细节

由于 JVM 的限制,我们建议在多平台项目中遵循这种文件命名方案:JVM 不允许顶层成员(函数、属性)存在。

为了绕过这一点,Kotlin JVM 编译器会创建包装类(即所谓的「文件门面」)来容纳顶层成员声明。文件门面的内部名称由文件名派生而来。

而 JVM 又不允许存在多个完全限定名(FQN)相同的类。这可能导致 Kotlin 项目无法编译到 JVM:

root
|- commonMain/kotlin/myPackage/Platform.kt // contains 'fun count() { }'
|- jvmMain/kotlin/myPackage/Platform.kt // contains 'fun multiply() { }'

这里两个 Platform.kt 文件位于同一个包中,因此 Kotlin JVM 编译器会生成两个文件门面,它们的 FQN 都是 myPackage.PlatformKt。这会产生「Duplicate JVM classes」错误。

避免该问题最简单的方法就是按照上面的指导重命名其中一个文件。这种命名方案既能帮助避免冲突,又能保持代码可读性。

提示: 有两种场景下这些建议看起来是多余的,但我们仍建议遵循它们:* 非 JVM 平台不存在文件门面重复的问题。不过这种命名方案有助于保持文件命名的一致性。* 在 JVM 上,如果源文件没有顶层声明,就不会生成文件门面,也就不会遇到命名冲突。但一次简单的重构或新增就可能引入顶层函数,从而产生同样的「Duplicate JVM classes」错误,这种命名方案有助于避免此类情况。

源文件内部组织

只要多个声明(类、顶层函数或属性)在语义上彼此密切相关,并且文件大小保持合理(不超过几百行),我们就鼓励把它们放在同一个 Kotlin 源文件中。

具体来说,当为某个类定义对该类的所有使用者都有意义的扩展函数时,请把它们与该类放在同一个文件中。当定义只对某个特定使用者有意义的扩展函数时,请把它们放在该使用者的代码旁边。避免为了集中存放某个类的所有扩展而单独创建文件。

类的布局

类的内容应按以下顺序排列:

  1. 属性声明和初始化块
  2. 次级构造器
  3. 方法声明
  4. 伴生对象

不要按字母顺序或可见性对方法声明排序,也不要把普通方法与扩展方法分开。相反,应把相关的内容放在一起,这样从上到下阅读这个类的人就能跟上其中发生的逻辑。选定一种顺序(高层内容在前,或者相反),并坚持下去。

把嵌套类放在使用这些类的代码旁边。如果这些类打算对外使用、而且没有在类内部被引用,请把它们放在最后,即伴生对象之后。

接口实现的布局

实现接口时,请让实现成员与接口成员的顺序保持一致(必要时可以在其中穿插用于实现的额外私有方法)。

重载的布局

始终把重载放在类中彼此相邻的位置。

命名规则

Kotlin 中包和类的命名规则相当简单:

  • 包名始终使用小写,并且不使用下划线(org.example.project)。通常不建议使用多词名称,但如果确实需要使用多个词,可以直接把它们连在一起,或者使用驼峰命名法(org.example.myProject)。

  • 类和对象的名称使用大驼峰命名法:

1
2
3
open class DeclarationProcessor { /*...*/ }

object EmptyDeclarationProcessor : DeclarationProcessor() { /*...*/ }

函数名

函数、属性和局部变量的名称以小写字母开头,使用驼峰命名法,并且不含下划线:

1
2
fun processDeclarations() { /*...*/ }
var declarationCount = 1

类式函数的命名

有两种例外情况,函数名应遵循类的命名规范。这类函数通常定义在顶层。

  • 用于创建类实例的工厂函数可以与抽象返回类型同名:
1
2
3
4
5
   interface Foo { /*...*/ }

   class FooImpl : Foo { /*...*/ }

   fun Foo(): Foo { return FooImpl() }
  • 返回 Unit 的 @Composable 函数:
1
   @Composable fun TabHeader { /*...*/ }

测试方法的命名

在测试中(且仅在测试中),你可以使用带空格的方法名,只需用反引号包起来。请注意,这类方法名从 API 级别 30 起才被 Android 运行时支持。测试代码中也可以使用下划线。

1
2
3
4
5
class MyTestCase {
    @Test fun `ensure everything works`() { /*...*/ }

    @Test fun ensureEverythingWorks_onAndroid() { /*...*/ }
}

属性命名

常量(用 const 标记的属性,或者没有自定义 get 函数、保存深层不可变数据的顶层或对象 val 属性)的名称应全部大写、以下划线分隔,遵循尖叫蛇形命名法:

1
2
const val MAX_COUNT = 8
val USER_NAME_FIELD = "UserName"

保存带行为的对象或可变数据的顶层或对象属性,其名称应使用驼峰命名法:

1
val mutableCollection: MutableSet<String> = HashSet()

保存单例对象引用的属性,其命名风格可以与 object 声明相同:

1
val PersonComparator: Comparator<Person> = /*...*/

对于枚举常量,可以根据用途选择全大写、下划线分隔的尖叫蛇形命名法(enum class Color { RED, GREEN })或大驼峰命名法。

幕后属性的命名

如果一个类有两个概念上相同、但一个是公开 API 一部分、另一个是实现细节的属性,请用下划线作为私有属性名称的前缀:

1
2
3
4
5
6
class C {
    private val _elementList = mutableListOf<Element>()

    val elementList: List<Element>
        get() = _elementList
}

选择好的名称

类的名称通常是名词或名词短语,说明这个类是什么:List、PersonReader。

方法的名称通常是动词或动词短语,说明这个方法做什么:close、readPersons。名称还应暗示该方法是修改对象还是返回一个新对象。例如,sort 表示就地排序集合,而 sorted 表示返回一个排序后的集合副本。

名称应清楚表达该实体的用途,因此最好不要使用无意义的词(Manager、Wrapper)。

当在声明名称中使用缩略语时,请遵循以下规则:

  • 对于两个字母的缩略语,两个字母都大写。例如 IOStream。
  • 对于超过两个字母的缩略语,只大写首字母。例如 XmlFormatter 或 HttpInputStream。

格式化

缩进

使用四个空格进行缩进。不要使用制表符。

对于花括号,请把左花括号放在该结构开始的那一行末尾,把右花括号单独放在一行,并与该结构的起始位置水平对齐。

1
2
3
4
5
if (elements != null) {
    for (element in elements) {
        // ...
    }
}

在 Kotlin 中分号是可选的,因此换行是有意义的。语言设计假定使用 Java 风格的花括号,如果你尝试使用其他格式化风格,可能会遇到意想不到的行为。

水平空白

  • 在二元运算符两侧加空格(a + b)。例外:不要在「区间到」运算符两侧加空格(0..i)。
  • 一元运算符两侧不加空格(a++)。
  • 在控制流关键字(if、when、for 和 while)与相应的左圆括号之间加空格。
  • 在主构造器声明、方法声明或方法调用的左圆括号之前不加空格。
1
2
3
4
5
6
7
class A(val x: Int)

fun foo(x: Int) { ... }

fun bar() {
    foo(1)
}
  • 绝不在 (、[ 之后,也不在 ]、) 之前加空格。
  • 绝不在 . 或 ?. 两侧加空格:foo.bar().filter { it > 2 }.joinToString()、foo?.bar()。
  • 在 // 之后加空格:// This is a comment。
  • 不要在用于指定类型参数的尖括号两侧加空格:class Map<K, V> { ... }。
  • 不要在 :: 两侧加空格:Foo::class、String::length。
  • 不要在用于标记可空类型的 ? 之前加空格:String?。

一般来说,应避免任何形式的水平对齐。把标识符重命名为长度不同的名字,不应影响声明或任何使用处的格式。

冒号

在以下场景中,请在 : 之前加空格:

  • 用于分隔类型与其父类型时。
  • 委托给父类构造器或同一类的另一个构造器时。
  • 在 object 关键字之后。

当 : 用于分隔声明与其类型时,不要在它之前加空格。

始终在 : 之后加空格。

1
2
3
4
5
6
7
8
9
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 { /*...*/ }
}

类头

主构造器参数不多的类可以写成一行:

1
class Person(id: Int, name: String)

类头较长时,应把每个主构造器参数放在单独一行并缩进。同时,右圆括号应另起一行。如果使用继承,父类构造器调用或所实现接口的列表应与圆括号位于同一行:

1
2
3
4
5
class Person(
    id: Int,
    name: String,
    surname: String
) : Human(id, name) { /*...*/ }

对于多个接口,父类构造器调用应放在最前面,然后每个接口各占一行:

1
2
3
4
5
6
class Person(
    id: Int,
    name: String,
    surname: String
) : Human(id, name),
    KotlinMaker { /*...*/ }

对于父类型列表很长的类,请在冒号之后换行,并把所有父类型名称水平对齐:

1
2
3
4
5
6
7
class MyFavouriteVeryLongClassHolder :
    MyLongHolder<MyFavouriteVeryLongClass>(),
    SomeOtherInterface,
    AndAnotherOne {

    fun foo() { /*...*/ }
}

当类头较长时,为了清楚地把类头与类体分开,可以在类头之后加一个空行(如上面的示例),或者把左花括号单独放在一行:

1
2
3
4
5
6
7
class MyFavouriteVeryLongClassHolder :
    MyLongHolder<MyFavouriteVeryLongClass>(),
    SomeOtherInterface,
    AndAnotherOne
{
    fun foo() { /*...*/ }
}

构造器参数使用常规缩进(四个空格)。这样可以确保在主构造器中声明的属性与在类体中声明的属性具有相同的缩进。

修饰符顺序

如果声明带有多个修饰符,请始终按以下顺序书写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public / protected / private / internal
expect / actual
final / open / abstract / sealed / const
external
override
lateinit
tailrec
vararg
suspend
inner
enum / annotation / fun // as a modifier in `fun interface`
companion
inline / value
infix
operator
data

把所有注解放在修饰符之前:

1
2
@Named("Foo")
private val foo: Foo

除非你在开发一个库,否则请省略多余的修饰符(例如 public)。

注解

把注解放在被标注声明之前的单独行上,并保持相同的缩进:

1
2
@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude

不带参数的注解可以放在同一行:

1
2
@JsonExclude @JvmField
var x: String

单个不带参数的注解可以与相应的声明放在同一行:

1
@Test fun foo() { /*...*/ }

文件注解

文件注解放在文件注释(如果有)之后、package 语句之前,并与 package 之间用一个空行分隔(以强调它们针对的是文件而不是包)。

1
2
3
4
/** License, copyright and whatever */
@file:JvmName("FooBar")

package foo.bar

函数

如果函数签名放不进一行,请使用以下语法:

1
2
3
4
5
6
fun longMethodName(
    argument: ArgumentType = defaultValue,
    argument2: AnotherArgumentType,
): ReturnType {
    // body
}

函数参数使用常规缩进(四个空格)。这有助于与构造器参数保持一致。

对于函数体只包含单个表达式的函数,优先使用表达式函数体。

1
2
3
4
5
fun foo(): Int {     // bad
    return 1
}

fun foo() = 1        // good

表达式函数体

如果函数使用表达式函数体,而它的第一行放不进声明所在的那一行,请把 = 号留在第一行,并把表达式函数体缩进四个空格。

1
2
fun f(x: String, y: String, z: String) =
    veryLongFunctionCallWithManyWords(andLongParametersToo(), x, y, z)

属性

对于非常简单的只读属性,可以考虑写成一行:

1
val isEmpty: Boolean get() = size == 0

对于更复杂的属性,请始终把 get 和 set 关键字放在单独的行上:

1
2
val foo: String
    get() { /*...*/ }

对于带初始化器的属性,如果初始化器较长,请在 = 号之后换行,并把初始化器缩进四个空格:

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

控制流语句

如果 if 或 when 语句的条件是多行的,请始终在语句体外加花括号。把条件的每一后续行相对于语句起始位置缩进四个空格。把条件的右圆括号与左花括号一起放在单独一行:

1
2
3
4
5
if (!component.isSyncing &&
    !hasAnyKotlinRuntimeInScope(module)
) {
    return createKotlinNotConfiguredPanel(module)
}

这有助于让条件与语句体对齐。

把 else、catch、finally 关键字,以及 do-while 循环中的 while 关键字,放在与前一个花括号同一行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
if (condition) {
    // body
} else {
    // else part
}

try {
    // body
} finally {
    // cleanup
}

在 when 语句中,如果某个分支超过一行,可以考虑用空行把它与相邻的分支块分开:

1
2
3
4
5
6
7
8
9
private fun parsePropertyValue(propName: String, token: Token) {
    when (token) {
        is Token.ValueToken ->
            callback.visitValue(propName, token.value)

        Token.LBRACE -> { // ...
        }
    }
}

短分支应写在条件同一行上,并且不加花括号。

1
2
3
4
when (foo) {
    true -> bar() // good
    false -> { baz() } // bad
}

方法调用

参数列表较长时,请在左圆括号之后换行。参数缩进四个空格。把关系密切的多个参数放在同一行。

1
2
3
4
5
drawSquare(
    x = 10, y = 10,
    width = 100, height = 100,
    fill = true
)

在分隔参数名与参数值的 = 两侧加空格。

链式调用的换行

链式调用换行时,请把 . 字符或 ?. 运算符放在下一行,并只缩进一级:

1
2
3
4
val anchor = owner
    ?.firstChild!!
    .siblings(forward = true)
    .dropWhile { it is PsiComment || it is PsiWhiteSpace }

链中的第一个调用通常应在它之前换行,但如果代码那样更清晰,省略换行也是可以的。

Lambda

在 lambda 表达式中,花括号两侧应有空格,用于分隔参数与函数体的箭头两侧也应有空格。如果调用只接受一个 lambda,尽可能把它放在圆括号之外。

1
list.filter { it > 10 }

如果要为 lambda 指定标签,不要在标签与左花括号之间加空格:

1
2
3
4
5
fun foo() {
    ints.forEach lit@{
        // ...
    }
}

在多行 lambda 中声明参数名时,请把参数名放在第一行,后面紧跟箭头并换行:

1
2
3
appendCommaSeparated(properties) { prop ->
    val propertyValue = prop.get(obj)  // ...
}

如果参数列表太长放不进一行,请把箭头单独放在一行:

1
2
3
4
5
6
foo {
    context: Context,
    environment: Env
    ->
    context.configureEnv(environment)
}

尾随逗号

尾随逗号是一系列元素中最后一个元素之后的逗号:

1
2
3
4
5
class Person(
    val firstName: String,
    val lastName: String,
    val age: Int, // trailing comma
)

使用尾随逗号有几个好处:

  • 它让版本控制中的差异更干净 —— 注意力都集中在被修改的值上。
  • 它让添加和重新排列元素更容易 —— 调整元素时不必新增或删除逗号。
  • 它简化了代码生成,例如对象初始化器。最后一个元素也可以带逗号。

尾随逗号完全是可选的 —— 即使不加,代码也照常工作。Kotlin 风格指南鼓励在声明处使用尾随逗号,而在调用处则由你自行决定。

要在 IntelliJ IDEA 格式化器中启用尾随逗号,请打开 Settings/Preferences | Editor | Code Style | Kotlin,选择 Other 标签页并勾选 Use trailing comma 选项。

枚举

1
2
3
4
5
6
enum class Direction {
    NORTH,
    SOUTH,
    WEST,
    EAST, // trailing comma
}

值实参

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fun shift(x: Int, y: Int) { /*...*/ }
shift(
    25,
    20, // trailing comma
)
val colors = listOf(
    "red",
    "green",
    "blue", // trailing comma
)

类属性与参数

1
2
3
4
5
6
7
8
class Customer(
    val name: String,
    val lastName: String, // trailing comma
)
class Customer(
    val name: String,
    lastName: String, // trailing comma
)

函数值参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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
) {}

类型可选的参数(包括 setter)

1
2
3
4
5
6
7
8
val sum: (Int, Int, Int) -> Int = fun(
    x,
    y,
    z, // trailing comma
): Int {
    return x + y + x
}
println(sum(8, 8, 8))

索引后缀

1
2
3
4
5
6
7
8
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
    ]

lambda 中的参数

1
2
3
4
5
6
7
8
9
fun main() {
    val x = {
            x: Comparable<Number>,
            y: Iterable<Number>, // trailing comma
        ->
        println("1")
    }
    println(x)
}

when 条目

1
2
3
4
5
6
7
fun isReferenceApplicable(myReference: KClass<*>) = when (myReference) {
    Comparable::class,
    Iterable::class,
    String::class, // trailing comma
        -> true
    else -> false
}

集合字面量(在注解中)

1
2
3
4
5
6
7
8
annotation class ApplicableFor(val services: Array<String>)
@ApplicableFor([
    "serializer",
    "balancer",
    "database",
    "inMemoryCache", // trailing comma
])
fun run() {}

类型实参

1
2
3
4
5
6
7
fun <T1, T2> foo() {}
fun main() {
    foo<
            Comparable<Number>,
            Iterable<Number>, // trailing comma
            >()
}

类型参数

1
2
3
4
class MyMap<
        MyKey,
        MyValue, // trailing comma
        > {}

解构声明

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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()

文档注释

对于较长的文档注释,请把开头的 /** 单独放在一行,并让后续每一行都以星号开头:

1
2
3
4
/**
 * This is a documentation comment
 * on multiple lines.
 */

较短的注释可以写在一行里:

1
/** This is a short documentation comment. */

一般来说,避免使用 @param 和 @return 标签。相反,应把参数和返回值的说明直接写入文档注释,并在提到参数的地方加上指向它们的链接。只有在需要较长描述、无法融入正文行文时,才使用 @param 和 @return。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Avoid doing this:

/**
 * 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): Int { /*...*/ }

// Do this instead:

/**
 * Returns the absolute value of the given [number].
 */
fun abs(number: Int): Int { /*...*/ }

避免冗余结构

一般来说,如果 Kotlin 中某种语法结构是可选的,并且被 IDE 标记为冗余,你就应该在代码中省略它。不要仅仅「为了清晰」而在代码中留下不必要的语法元素。

Unit 返回类型

如果函数返回 Unit,应省略返回类型:

1
2
3
fun foo() { // ": Unit" is omitted here

}

分号

尽可能省略分号。

字符串模板

把简单变量插入字符串模板时不要使用花括号。只在较长表达式中使用花括号:

1
println("$name has ${children.size} children")

使用多美元字符串插值把美元符号字符 $ 当作字符串字面量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
val KClass<*>.jsonSchema : String
    get() = $$"""
        {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "$id": "https://example.com/product.schema.json",
            "$dynamicAnchor": "meta",
            "title": "$${simpleName ?: qualifiedName ?: "unknown"}",
            "type": "object"
        }
        """

语言特性的习惯用法

不可变性

优先使用不可变数据而不是可变数据。如果局部变量和属性在初始化之后不会被修改,请始终声明为 val 而不是 var。

声明不会被修改的集合时,始终使用不可变的集合接口(Collection、List、Set、Map)。使用工厂函数创建集合实例时,尽可能使用返回不可变集合类型的函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Bad: use of a mutable collection type for value which will not be mutated
fun validateValue(actualValue: String, allowedValues: HashSet<String>) { ... }

// Good: immutable collection type used instead
fun validateValue(actualValue: String, allowedValues: Set<String>) { ... }

// Bad: arrayListOf() returns ArrayList<T>, which is a mutable collection type
val allowedValues = arrayListOf("a", "b", "c")

// Good: listOf() returns List<T>
val allowedValues = listOf("a", "b", "c")

参数默认值

优先声明带参数默认值的函数,而不是声明重载函数。

1
2
3
4
5
6
// Bad
fun foo() = foo("a")
fun foo(a: String) { /*...*/ }

// Good
fun foo(a: String = "a") { /*...*/ }

类型别名

如果你有一个函数类型或带类型参数的类型在代码库中被多次使用,最好为它定义一个类型别名:

1
2
typealias MouseClickHandler = (Any, MouseEvent) -> Unit
typealias PersonIndex = Map<String, Person>

如果你为了避免名称冲突而使用私有或 internal 的类型别名,请优先使用包与导入中提到的 import ... as ...。

Lambda 参数

在简短且不嵌套的 lambda 中,建议使用 it 约定,而不是显式声明参数。在带参数的嵌套 lambda 中,始终显式声明参数。

Lambda 中的返回

避免在 lambda 中使用多个带标签的返回。可以考虑重构该 lambda,使它只有一个出口。如果做不到,或者这样做不够清晰,可以考虑把 lambda 转换为匿名函数。

不要对 lambda 中的最后一条语句使用带标签的返回。

具名参数

当方法接收多个相同基本类型的参数,或接收 Boolean 类型的参数时,请使用具名参数语法,除非所有参数的含义从上下文中一看便知。

1
drawSquare(x = 10, y = 10, width = 100, height = 100, fill = true)

条件语句

优先使用 try、if 和 when 的表达式形式。

1
return if (x) foo() else bar()
1
2
3
4
return when(x) {
    0 -> "zero"
    else -> "nonzero"
}

上面的写法优于下面这种:

1
2
3
4
if (x)
    return foo()
else
    return bar()
1
2
3
4
when(x) {
    0 -> return "zero"
    else -> return "nonzero"
}

if 与 when 的取舍

对于二选一的条件,优先使用 if 而不是 when。例如,使用这种 if 写法:

1
if (x == null) ... else ...

而不是这种 when 写法:

1
2
3
4
when (x) {
    null -> // ...
    else -> // ...
}

如果有三个或更多选项,则优先使用 when。

when 表达式中的守卫条件

在 when 表达式或语句中使用守卫条件组合多个布尔表达式时,请使用圆括号:

1
2
3
when (status) {
    is Status.Ok if (status.info.isEmpty() || status.info.id == null) -> "no information"
}

而不是:

1
2
3
when (status) {
    is Status.Ok if status.info.isEmpty() || status.info.id == null -> "no information"
}

条件中的可空布尔值

如果你需要在条件语句中使用可空的 Boolean,请使用 if (value == true) 或 if (value == false) 进行检查。

循环

优先使用高阶函数(filter、map 等)而不是循环。例外是 forEach(除非 forEach 的接收者是可空的,或者 forEach 属于较长的调用链,否则优先使用普通的 for 循环)。

在使用多个高阶函数的复杂表达式与循环之间做选择时,请理解每种写法所执行操作的开销,并把性能因素考虑在内。

区间的循环

使用 ..< 运算符遍历左闭右开的区间:

1
2
for (i in 0..n - 1) { /*...*/ }  // bad
for (i in 0..<n) { /*...*/ }  // good

字符串

优先使用字符串模板而不是字符串拼接。

优先使用多行字符串,而不是在普通字符串字面量中嵌入 \n 转义序列。

要在多行字符串中保持缩进,当结果字符串不需要任何内部缩进时使用 trimIndent;当需要内部缩进时使用 trimMargin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fun main() {
    println("""
     Not
     trimmed
     text
     """
    )

    println("""
     Trimmed
     text
     """.trimIndent()
    )

    println()

    val a = """Trimmed to margin text:
            |if(a > 1) {
            |    return a
            |}""".trimMargin()

   println(a)
}

了解 Java 与 Kotlin 多行字符串之间的区别。

函数与属性的取舍

在某些场景中,无参数的函数与只读属性可以互换。虽然语义相似,但在选择其中一个时有一些风格上的约定。

当底层算法满足以下条件时,优先使用属性而不是函数:

  • 不会抛出异常。
  • 计算开销很小(或者只在首次运行时缓存)。
  • 在对象状态未改变的情况下,每次调用都返回相同结果。

扩展函数

请放心地使用扩展函数。每当你有一个主要作用于某个对象的函数时,都可以考虑把它写成以该对象为接收者的扩展函数。为尽量减少 API 污染,请在合理的范围内限制扩展函数的可见性。必要时可以使用局部扩展函数、成员扩展函数,或具有 private 可见性的顶层扩展函数。

中缀函数

只有当函数作用于两个角色相似的对象时,才把它声明为 infix。好的例子:and、to、zip。不好的例子:add。

如果方法会修改接收者对象,就不要把它声明为 infix。

工厂函数

如果你为某个类声明工厂函数,请避免让它与类本身同名。最好使用一个独特的名称,清楚表明这个工厂函数的行为为何特殊。只有在确实没有特殊语义时,才可以与类同名。

1
2
3
4
5
class Point(val x: Double, val y: Double) {
    companion object {
        fun fromPolar(angle: Double, radius: Double) = Point(...)
    }
}

如果你的对象有多个重载构造器,它们既不调用不同的父类构造器,也无法简化为一个包含带默认值参数的构造器,那么最好用工厂函数替换这些重载构造器。

平台类型

返回平台类型表达式的公开函数/方法必须显式声明其 Kotlin 类型:

1
fun apiCall(): String = MyJavaApi.getProperty("name")

任何用平台类型表达式初始化的属性(无论包级还是类级)都必须显式声明其 Kotlin 类型:

1
2
3
class Person {
    val name: String = MyJavaApi.getProperty("name")
}

用平台类型表达式初始化的局部值可以有类型声明,也可以没有:

1
2
3
4
fun main() {
    val name = MyJavaApi.getProperty("name")
    println(name)
}

作用域函数 apply/with/run/also/let

Kotlin 提供了一组函数,用于在给定对象的上下文中执行一段代码:let、run、with、apply 和 also。关于如何为你的场景选择合适的作​​用域函数,请参阅作用域函数。

库的编码规范

在编写库时,建议遵循一套额外的规则以确保 API 稳定性:

  • 始终显式指定成员可见性(避免意外把声明暴露为公开 API)。
  • 始终显式指定函数返回类型和属性类型(避免在实现变化时意外改变返回类型)。
  • 为所有公开成员提供 KDoc 注释,不需要新增文档的重写成员除外(以支持为库生成文档)。

关于为你的库编写 API 时的最佳实践和需要考虑的要点,请参阅库作者指南。