编码约定
通用且易于遵循的编码规范对于任何编程语言都至关重要。 在这里,我们提供了使用 Kotlin 的项目的代码风格和代码组织指南。
在 IDE 中配置风格
Kotlin 的两个最流行的 IDE - IntelliJ IDEA 和Android Studio 提供了强大的代码风格支持。 您可以配置它们以使您的代码自动符合给定的代码风格。
应用代码风格指南
转到 Settings/Preferences | Editor | Code Style | Kotlin。
点击 Set from...。
选择 Kotlin style guide(Kotlin 样式指南)。
验证您的代码是否遵循代码风格
转到 Settings/Preferences | Editor | Inspections | General。
打开 Incorrect formatting 检查。 额外的检查,验证代码风格指南中描述的其他问题(如命名约定)默认已启用。
源代码组织
目录结构
在纯 Kotlin 项目中,推荐的目录结构遵循包结构,省略了共同的根包。 例如,如果项目中的所有代码都在 org.example.kotlin
包及其子包中,那么带有 org.example.kotlin
包的文件应直接放在源目录下,而 org.example.kotlin.network.socket
中的文件应该位于源目录的 network/socket
子目录下。
源文件命名
如果一个 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 的限制,我们建议在跨平台项目中遵循这种文件命名方案:不允许顶级成员(函数、属性)。
为了解决这个问题,Kotlin JVM 编译器会创建包含顶级成员声明的包装类(所谓的“文件门面(file facades)”)。文件门面的内部名称派生自文件名。
反过来,JVM 不允许具有相同 完全限定名(FQN,fully qualified name) 的多个类。这可能导致 Kotlin 项目无法编译到 JVM:
在这里, Platform.kt
文件都在相同的包中,因此 Kotlin JVM 编译器会生成两个文件门面,它们的 FQN 都是 myPackage.PlatformKt
。 这会导致 "Duplicate JVM classes(重复的 JVM 类)" 错误。
最简单的避免方法是根据上述准则之一重命名其中一个文件。这种命名方案有助于避免冲突同时保持代码可读性。
源文件组织
我们鼓励将多个声明(类、顶层函数或属性)放置在同一个 Kotlin 源文件中,只要这些声明在语义上彼此密切相关,并且文件大小保持合理(不超过几百行)。
特别是在为一个类定义适用于该类所有客户端的扩展函数时,请将它们放在与该类本身相同的文件中。 在为仅对特定客户端有意义的扩展函数时,将它们放在该客户端代码的旁边。 避免仅为容纳某个类的所有扩展而创建文件。
类布局
类的内容应按以下顺序排列:
属性声明和初始化块
辅助构造函数
方法声明
伴生对象
不要按字母顺序或可见性对方法声明进行排序,也不要将常规方法与扩展方法分开。 相反,将相关的内容放在一起,以便从上到下阅读类的人能够理解发生的逻辑。 选择一种顺序(首先是更高级别的内容,或相反),并坚持遵循它。
将嵌套类放在使用这些类的代码旁边。如果这些类旨在被外部使用且在类内部未被引用,请将它们放在伴生对象之后的末尾。
接口实现布局
在实现接口时,将实现的成员保持与接口成员相同的顺序(必要时,与用于实现的额外私有方法交错)。
重载布局
始终将重载放在类中彼此相邻的位置。
命名规则
包名总是小写并且不使用下划线(
org.example.project
)。 通常不建议使用多个单词的包名,但如果确实需要使用多个单词,可以直接将它们连接在一起,或者使用驼峰命名法(org.example.myProject
)。类和对象的名称使用大驼峰命名法:
函数名称
函数、属性和局部变量的名称以小写字母开头,并使用驼峰命名法,不使用下划线:
例外:用于创建类实例的工厂函数可以与抽象返回类型同名:
测试方法的命名
在测试中(仅在测试中),你可以使用用反引号括起来的带空格的方法名。 注意,这样的方法名仅在 Android 运行时 API 级别 30 及以上版本支持。 测试代码中的方法名也允许使用下划线。
属性命名
常量的名称(标记为 const
的属性,或没有自定义 get
函数且保存深度不可变数据的顶层或对象 val
属性)应使用全大写字母,单词之间用下划线分隔,遵循 (尖叫蛇命名法) 约定:
持有具有行为或可变数据的对象的顶级或对象属性应该使用驼峰命名法:
持有对单例对象引用的属性可以使用与object
声明相同的命名风格:
对于枚举常量,可以使用全大写、下划线分隔的 (尖叫蛇命名法) 名称 (enum class Color { RED, GREEN }
),也可以使用大驼峰命名法,具体取决于使用场景。
幕后属性的名称
如果一个类有两个在概念上相同,但一个是公共 API 的一部分,另一个是实现细节的属性,请使用下划线作为私有属性名称的前缀:
选择良好的命名
类的名称通常是一个名词或一个解释类是什么的名词短语: List
(列表), PersonReader
(人员阅读器)。
方法的名称通常是一个动词或一个解释方法做什么的动词短语: close
(关闭), readPersons
(读取人员)。 名称还应该表明方法是否改变对象或返回一个新对象。例如, sort
(排序)正在就地对集合进行排序,而 sorted
(已排序)返回集合的已排序副本。
名称应该清楚地表明实体的目的,因此最好避免在名称中使用无意义的词汇(Manager
(管理器), Wrapper
(包装器))。
在声明名称中使用首字母缩写时,如果它由两个字母组成,则将其大写(IOStream
);如果它更长,则仅大写第一个字母(XmlFormatter
, HttpInputStream
)。
格式化
缩进
使用四个空格进行缩进,不要使用制表符。
对于花括号,将开放的花括号放在构造开始的行的末尾,将闭合的花括号放在与开放构造横向对齐的单独行上。
水平空白
在二元运算符周围加上空格(
a + b
)。例外:在“范围运算符(range to)”(0..i
)周围不加空格。不要在一元运算符周围加上空格(
a++
)。在控制流关键字(
if
、when
、for
和while
)与相应的开括号之间加上空格。在主构造函数声明、方法声明或方法调用中,不要在开括号前加上空格。
永远不要在
(
、[
后或]
、)
前加空格。永远不要在
.
或?.
周围加空格:foo.bar().filter { it > 2 }.joinToString()
,foo?.bar()
。在
//
后加一个空格:// This is a comment
。不要在用于指定类型参数的尖括号周围加空格:
class Map<K, V> { ... }
。不要在
::
周围加空格:Foo::class
,String::length
。在
?
前不要加空格,它用于标记可空类型:String?
。
作为一般规则,避免任何形式的水平对齐。将标识符重命名为不同长度的名称不应该影响声明或者任何用法的格式。
冒号
在以下场景中,冒号前要加空格:
当它用于分隔类型和超类型时。
当委托给父类构造函数或同一类的其他构造函数时。
在
object
关键字后。
当冒号用于分隔声明和其类型时,不要在冒号前加空格。
始终在冒号后加空格。
类头部
具有少量主构造函数参数的类可以写在单行上:
具有较长头部的类应该进行格式化,使每个主构造函数参数都在单独的一行,并进行缩进。 此外,闭括号应该在新的一行上。如果使用继承,超类构造函数调用或实现的接口列表应该位于与括号相同的行上:
对于多个接口,应该放置超类构造函数调用在第一位,然后每个接口应该位于不同的行:
对于具有长超类型列表的类,在冒号后进行换行,并水平对齐所有超类的名称:
为了在类头部较长时清晰地分隔类头和类体,可以在类头部后面加上一个空行(如上面的示例)或将开花括号放在单独的一行:
对于构造函数参数,请使用常规缩进(四个空格)。这确保在主构造函数中声明的属性与类体中声明的属性具有相同的缩进。
修饰符顺序
如果一个声明有多个修饰符,请按照以下顺序放置它们:
将所有注解放在修饰符之前:
除非你在开发一个库,否则省略冗余的修饰符(例如, public
)。
注解
将注解放在它们所附加的声明之前的单独行上,并且使用相同的缩进:
没有参数的注解可以放在同一行上:
没有参数的单个注解可以与相应的声明放在同一行上:
文件注解
文件注解放在文件注释(如果有的话)之后,在 package
语句之前,与 package
之间用一个空行分隔(以强调它们针对的是文件而不是包)。
函数
如果函数签名不适合放在一行上,请使用以下语法:
对于函数参数,请使用常规缩进(四个空格)。这有助于确保与构造函数参数的一致性。
对于函数体只包含单个表达式的函数,最好使用表达式体。
表达式体
如果函数有一个表达式体,其第一行不能放在与声明相同的行上,请将 =
符号放在第一行,并将表达式体缩进四个空格。
属性
对于非常简单的只读属性,请考虑单行格式化:
对于更复杂的属性,请始终将 get
和 set
关键字放在不同的行上:
对于带有初始化程序的属性,如果初始化程序很长,请在 =
符号之后添加一个换行,并将初始化程序缩进四个空格:
控制流语句
如果 if
或 when
语句的条件跨足多行,请始终在语句体周围使用花括号。 相对于语句的开始,将条件的每一行缩进四个空格。 将条件的闭括号与开花括号放在单独的一行上:
这有助于对齐条件和语句体。
将 else
、 catch
、 finally
关键字,以及 do-while
循环的 while
关键字放在前面花括号的同一行:
在 when
语句中,如果一个分支超过一行,请考虑在它与相邻的 case 块之间加上一个空行:
将短的分支放在与条件相同的行上,不使用花括号。
方法调用
在较长的参数列表中,在开括号后进行换行。通过四个空格缩进参数。 将多个密切相关的参数组合在同一行上。
在分隔参数名和值的 =
符号周围加上空格。
包装链式调用
在包装链式调用时,将 .
字符或 ?.
操作符放在下一行,缩进单倍空格:
链中的第一个调用通常应该在其前面有一个换行,但如果代码以这种方式更有意义,也可以省略。
Lambda 表达式
在 Lambda 表达式中,应在花括号周围以及将 参数 与 代码体 分隔的箭头周围使用空格。如果调用接受一个 Lambda,请尽可能将其放在括号外传递。
如果为 Lambda 分配一个标签,请不要在标签和开放花括号之间加上空格:
在多行 Lambda 中声明参数名称时,请将名称放在第一行,然后是箭头和换行符:
如果参数列表太长无法放在一行上,请将箭头放在单独的一行上:
尾随逗号
尾随逗号是一系列元素中最后一个元素之后的逗号符号:
使用尾随逗号有几个好处:
使版本控制的差异更清晰 - 因为所有的焦点都在更改的值上。
使添加和重新排序元素变得简单 - 如果操作元素,就无需添加或删除逗号。
简化代码生成,例如,对于对象初始化器。最后一个元素也可以有一个逗号。
尾随逗号是完全可选的 - 没有它们,你的代码仍然可以正常工作。Kotlin 风格指南鼓励在声明处使用尾逗号,并将其作为在调用处的自由裁量。
要在 IntelliJ IDEA 格式化器中启用尾逗号,请转到 Settings/Preferences | Editor | Code Style | Kotlin(设置/首选项 | 编辑器 | 代码样式 | Kotlin), 打开 Other(其他) 选项卡,并选择 Use trailing comma(使用尾随逗号) 选项。
枚举
值参数
类属性和参数
函数值参数
具有可选类型的参数(包括设置器)
索引后缀
lambda 中的参数
when 入口
集合字面量(在注解中)
类型实参
类型形参
解构声明
文档注释
对于较长的文档注释,请将起始符 /**
放在独立的一行,并在后续每一行以星号开头:
简短注释可以放在单独的一行:
通常避免使用 @param
和 @return
标签。 相反,将参数和返回值的描述直接融入文档注释中,并在提及参数的地方添加参数链接。 仅在需要提供无法融入主文本流的详细描述时使用 @param
和 @return
。
避免冗余结构
通常情况下,如果 Kotlin 中的某种语法结构是可选的并且由 IDE 标识为冗余的,应该在代码中省略它。 不要在代码中保留不必要的语法元素仅仅是为了“清晰”。
Unit 返回类型
如果函数返回 Unit,应省略返回类型:
分号
尽可能地省略分号。
字符串模板
在将简单变量插入字符串模板时,不要使用花括号。仅在表达式较长时使用花括号。
语言特性的惯用方式
不可变性
更倾向于使用不可变数据而非可变数据。如果在初始化后不会修改,始终将局部变量和属性声明为 val
而不是 var
。
总是使用不可变集合接口(Collection
, List
, Set
, Map
)声明不会被修改的集合。 在使用工厂函数创建集合实例时,尽量使用返回不可变集合类型的函数:
默认参数值
更倾向于使用带有默认参数值的函数声明,而不是声明重载函数。
类型别名
如果在代码库中多次使用功能型类型或具有类型形参的类型,请更倾向于为其定义一个类型别名:
如果使用私有或内部类型别名以避免名称冲突,更倾向于使用 包和导入 中提到的 import ... as ...
。
Lambda 参数
在简短且不嵌套的 Lambda 中,建议使用 it
约定而不是显式声明参数。在带有参数的嵌套 Lambda 中,始终显式声明参数。
Lambda 中的返回
避免在 Lambda 中使用多个带标签的返回。考虑重构 Lambda 以使其具有单一的退出点。如果这不可行或不够清晰,请考虑将 Lambda 转换为匿名函数。
不要对 Lambda 中的最后一条语句使用带标签的返回。
具名函数
当方法接受多个相同原始类型的参数,或者是 Boolean
类型的参数时,请使用具名函数语法,除非从上下文中完全清晰地了解所有参数的含义。
条件语句
优先使用 try
、 if
和 when
的表达式形式。
上述内容优于:
if 与 when 的选择
在二元条件中,推荐使用 if
而不是 when
。 例如,使用以下 if
语法:
而不是以下 when
语法:
如果有三个或更多选项,请优先使用 when
。
在条件语句中使用可空布尔值
如果需要在条件语句中使用可空的 Boolean
,请使用 if (value == true)
或 if (value == false)
进行检查。
循环
优先使用高阶函数(filter
、 map
等)而不是循环。 例外情况: forEach
(优先使用普通的 for
循环,除非 forEach
的接收者可空或 forEach
作为较长调用链的一部分使用)。
在选择使用多个高阶函数和循环之间时,了解每种情况下执行的操作开销,并考虑性能方面的考虑。
区间上的循环
使用 ..<
运算符循环遍历开放区间:
字符串
优先使用字符串模板而不是字符串连接。
优先使用多行字符串而不是将 \n
转义序列嵌入常规字符串文字中。
为保持多行字符串的缩进,当生成的字符串不需要任何内部缩进时,请使用 trimIndent
,或者当需要内部缩进时,请使用 trimMargin
:
了解 Java 和 Kotlin 多行字符串 之间的区别。
函数与属性的选择
在某些场景中,没有参数的函数可能与只读属性可互换。 虽然语义相似,但在何时优先使用其中之一,有一些风格约定。
当底层算法:
不会抛出异常。
计算开销小(或在第一次运行时进行缓存)。
如果对象状态没有变化,则每次调用返回相同的结果。
优先使用属性而不是函数。
扩展函数
你可以自由地使用扩展函数。每当有一个主要作用于对象的函数时,考虑将其作为接收该对象的扩展函数。 为了最小化 API 污染,根据情况限制扩展函数的可见性。 必要时使用局部扩展函数、成员扩展函数,或者具有私有可见性的顶层扩展函数。
中缀函数
只有在函数作用于两个扮演相似角色的对象时才将函数声明为 infix
。良好的例子包括: and
、 to
、 zip
。不好的例子是: add
。
如果方法会改变接收者对象,请勿将其声明为 infix
。
工厂函数
如果为类声明工厂函数,请避免将其命名为与类本身相同的名称。 最好使用一个独特的名称,清楚表明工厂函数的行为为何特殊。 仅当真的没有特殊语义时,才可以使用与类相同的名称。
如果你有一个对象有多个重载的构造函数,并且它们不调用不同的超类构造函数,也不能简化为带有默认参数值的单个构造函数,建议用工厂函数替换重载的构造函数。
平台类型
返回平台类型表达式的公共函数/方法必须明确声明其 Kotlin 类型:
任何使用平台类型表达式初始化的属性(包级别或类级别)都必须明确声明其 Kotlin 类型:
使用平台类型表达式初始化的局部值可能具有类型声明,也可能没有类型声明:
作用域函数 apply/with/run/also/let
Kotlin 提供了一组函数,用于在给定对象的上下文中执行一块代码: let
、 run
、 with
、 apply
和 also
。 关于在特定情况下选择正确的作用域函数的指导,请参考作用域函数。
库的编码约定
在编写库时,建议遵循一组额外的规则,以确保 API 的稳定性:
始终明确指定成员的可见性(以避免意外地将声明暴露为公共 API)。
始终明确指定函数返回类型和属性类型(以避免在实现更改时意外改变返回类型)。
为所有公共成员提供 KDoc 注释,覆盖的成员除外,且这些覆盖不需要任何新文档(以支持生成库的文档)。
了解更多关于编写库 API 时的最佳实践和需要考虑的想法,请参阅 库作者指南。