BAT使用的Kotlin编码规范
作者:互联网
BAT某大厂内部使用的Kotlin编码规范,在Kotlin官方规范的基础上又补充了不少内容,很有参考价值。
一 代码组织
-
【强制】在混合Java源码项目中,Kotlin 源文件应当与 Java 源文件位于同一源文件根目录下, 无需按照文件类型分开放置
-
【强制】如果源文件中只包含单个类,则以这个类名作为该文件名
-
【强制】一个类的内容按以下顺序排列(PS:官方推荐伴生对象放到最尾,但是源码里也有放到顶部的case,但是不要放在中间)
- 属性声明与初始化块 - 次构造函数 - 方法声明 - 伴生对象
-
【强制】在实现一个接口时,实现成员的顺序应该与该接口的成员顺序相同
二 命名规范
- 【强制】包的名称使用小写且不允许出现下划线
import com.intellij.openapi.action_system // 反例
- 【强制】包名点分隔符之间的名称尽量简短,避免使用多个单词的名称,若确实需要使用多个单词,则将小写字母连接在一起,避免使用驼峰命名
import org.example.myproject // 正例
import org.example.myProject // 反例
- 【强制】对象声明(单例类)的名称和普通类一样,以大写字母开头并使用驼峰
object Comparator {...}
- 【强制】泛型类型用全大写字母表示
// 反例:泛型没有全大写
class Box<Type>(t: Type) {
var value = t
}
- 【强制】常量使用大写、下划线分隔的名称,力求语义清晰,不要嫌名字长
【说明】常量包括const
的属性,或者顶层val
属性Type
【例外】有行为或者有custom getter的对象除外
const val MAX_COUNT = 8
val USER_NAME_MAP = mapOf("UserName", ...)
- 【强制】枚举常量使用大写、下划线分隔的名称
enum class Color { RED, GREEN }
三 代码格式
- 【强制】类/接口如果没有body内容,则省略花括号
class EmptyClass {} // 反例
interface EmptyInterface {} // 反例
object EmptyObject {} // 反例
- 【强制】对于由单个表达式构成的函数体,优先使用表达式形式。
fun foo() = 1 // 正例
fun foo(): Int { return 1 } // 反例
- 【强制】自定义 get 与 set方法如果包含代码块,需要将get、set、换行写
// 简单的get不许换行
val isEmpty: Boolean get() = size == 0
// get有代码块 换行写
val foo: String
get() { /*...*/ }
- 【强制】
when
语句中 将短分支放在与条件相同的行上,无需花括号。
// 正例:
when (foo) {
true -> bar()
false -> baz()
}
// 反例:
when (foo) {
true -> { bar() }
false -> { baz() }
}
- 【强制】定义方法时,如果有lambda表达式,则作为最后一个参数。有多个则尽量选择一个使用优先级最高的lambda放在最后。
// 正例:
fun <T, C: MutableCollection<in T>>
List<T>.filterTo(destination: C, predicate: (T) -> Boolean): C
// 反例:
fun <T, C: MutableCollection<in T>>
List<T>.filterTo(predicate: (T) -> Boolean, destination: C): C
四 惯用语法
-
【强制】声明变量时默认使用非空类型
-
【强制】能保证在使用时一定非空的成员,使用
lateinit
而不是可空类型。
【说明】注意Activity恢复重建等可能会影响初始化的流程,此时也应该保证lateinit
变量在使用时非空 -
【强制】尽可能使用
val
而不是var
-
【强制】尽可能减少类、成员的open声明
-
【强制】尽可能使用
List
/Set
/Map
,而不是MutableList
/MutableSet
/MutableMap
-
【强制】连接多个变量时,使用字符串模板
val fullName = "${user.firstName} ${user.lastName}" //正例
val fullName = user.firstName + " " + user.lastName // 反例
- 【强制】单行函数省略大括号,返回值类型明确的情况下也可省略
fun foo(a: Int, b: Int) = a + b // 正例
fun foo(a: Int, b: Int): Int { return a + b } // 反例
- 【强制】调用函数的最后一个参数是函数类型时,省略圆括号
observable.subscribe({ /*...*/ }) // 反例
observable.subscribe { /*...*/ } // 正例
- 【强制】使用
==
替代equals
if (this::class.java.equals(clazz)) // 反例
if (this::class.java == clazz) // 正例
- 【强制】在使用已经做过类型检查的变量时无需做类型转换
if (x is String) { print(x.length) } // 正例
if (x is String) { print((x as String).length) } // 反例
- 【强制】通过
as?
进行运行时类型检查
println((s as? Int) ?: 0) // 正例
if (s is Int) println(s) else println(0) // 反例
-
【强制】
when
语句结尾需要加else
,以防出现逻辑错误
【例外】密封类只需要列举所有子类 -
【强制】利用表达式语句,避免if语句中出现多个
return
// 正例:
fun isPass(score: Score) : Boolean {
return if (score >= 60) {
...
true
} else {
...
false
}
}
// 反例:
fun isPass(score: Score) : Boolean {
if ( score >= 60) {
...
return true
} else {
..
return false
}
}
- 【强制】for循环中使用
until
替代size - 1
// 反例:
for (i in 0..(list.size - 1)) {
...
}
// 正例:
for (i in 0 until list.size) {
...
}
- 【强制】连续对同一个变量进行读/写操作时,使用作用域函数
【说明】连续访问可空变量需要多次?.
每次都会进行判空。但使用?.
+作用域函数时,作用域内部引用的都是final并且非空的变量,减少判空次数。
val member: User?
// 反例:
fun initView(){
tvName.text = member?.name
tvAge.text = member?.age
member?.lastAccessedTime = System.currentTimeMillis()
button.setOnClickListener{ member?.selected = !member?.selected }
}
// 正例:
fun initView(){
member?.apply{
tvName.text = name
tvAge.text = age
lastAccessedTime = System.currentTimeMillis()
}
button.setOnClickListener{ member?.apply{ selected = !selected} }
}
调用链中保持原类型(T -> T) | 调用链中转换为其他类型(T -> R) | 调用链起始(考虑使用) | 调用链中应用条件语句(T -> T) | |
---|---|---|---|---|
多写操作 | T.apply { … } | T.run{ … } | with(T) { … } | T.takeIf/T.takeUnless |
多读操作 | T.also { … } | T.let{ … } | - | - |
- 【强制】对于初始化成本较高的变量,使用lazy进行惰性初始化;初始化成本不高则避免使用
【说明】滥用lazy会创建额外对象,增加性能开销
// 反例:创建空list对象的成本不高,不要使用lazy
val list by lazy { emptyList<Any>() }
- 【强制】lazy代理默认是线程安全的,单线程环境使用
LazyThreadSafeMode.NONE
提高性能
val lazyString: String by lazy(LazyThreadSafetyMode.NONE) { }
- 【强制】需要声明基本类型数组时,优先使用原生类型数组,而不要使用泛型数组
【说明】向泛型数组中添加基本类型会产生额外装箱开销
fun function(array: IntArray) { } // 正例
fun function(array: Array<Int>) { } // 反例
- 【强制】
vararg
的参数只允许使用字面值/数组构造器,不允许传递其他vararg
或数组的引用。
【说明】传递其他vararg
或数组的引用,会造成额外的数组拷贝,影响性能
//definition
fun multiString(vararg arg: String){...}
//bad:call with array
fun stringArray(array: Array<String>){ // array will has a extra copy
multiString(*array)
}
//bad:call with vararg
fun multiStringAnother(vararg arg: String){ // arg will has a extra copy
multiString(*arg)
}
multiString("1", "2", "3") // good:call with literal
multiString(*arrayOf("1", "2", "3")) // good:call with creation
multiString(*Array(3) { it.toString() }) // good:call with creation
五 类与对象
-
【强制】不对外部模块公开的非私有类、方法以及属性,需要添加
internal
修饰符 -
【强制】除非内部类需要访问外部类成员,否则不要为该类添加使用
inner
修饰符 -
【强制】open的子类中,override的成员默认是open的,如果确定其后续不应该再被继承,必须添加
final
修饰符
open class Foo() : Bar() {
final override fun test() { // 不需要子类重写,需添加final
super.test()
}
- 【强制】数据类中不允许有
var
属性。使用数据类的copy
方法进行属性的变更。
// 反例:数据类应该使用val属性
data class MutableDataClass(var i: Int) {
var s: String? = null
}
- 【强制】伴生对象中定义常量,需要加
const
修饰符。对象声明(单例类)中同理
// 正例
class MainFragment: Fragment() {
companion object {
const val TYPE_VIEW_HEADER = 0
const val TYPE_VIEW_FOOTER = 1
}
}
// 正例
object UserRepo {
const val USER_TYPE_ADMIN = "USER_TYPE_ADMIN"
}
- 【强制】使用object声明代替只有companion object的类(本类或者父类里没有其他类成员)
// 反例:
class MyClass{
companion object{
fun doSth(){...}
}
}
// 正例:
object MyClass{
fun doSth(){...}
}
六 函数与lambda表达式
- 【推荐】调用多参数函数时,尽量使用命名参数提高代码的可读性
//declaration
fun reformat(str: String,
normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ' ') { /*...*/ }
// 正例:
reformat(str,
normalizeCase = true,
upperCaseFirstLetter = true,
divideByCamelHumps = false,
wordSeparator = '_'
)
// 反例:
reformat(str, true, true, false, '_')
- 【推荐】优先使用顶层扩展函数替代静态工具类
// 反例:工具类Collections的静态方法
object Collections {
fun <T> sort(list: List<T>, c: Comparator<in T>) : List { ... }
}
// 正例:顶层扩展函数
fun <T> List<T>.sort(c: Comparator<in T>) : List<T> { ... }
- 【推荐】代码中需要多次使用的函数类型,可以定一个类型别名。注意为参数添加命名
typealias MouseClickHandler = (payload: Any, event: MouseEvent) -> Unit
- 【推荐】声明函数类型时,不要省略变量名,有利于调用处的IDE的代码补全
// 正例:
fun performRequest (
url: String,
callback: (code: Int, cotentL String) -> Unit
) { ... }
// 反例:
fun performRequest (
url: String,
callback: (Int, String) -> Unit
) { ... }
- 【推荐】lambda表达式的block中,如果主要进行对某个实例的写操作,则该实例声明为Receiver;如果主要是读操作,则该实例声明为参数。
inline fun <T> T.apply(block: T.() -> Unit): T//对T进行写操作,优先使用apply
tvName.apply {
text = "Jacky"
textSize = 20f
}
inline fun <T> T.also(block: (T) -> Unit): T //对T进行读操作 优先使用also
user.also {
tvName.text = it.name
tvAge.text = it.age
}
- 【强制】参数包含lambda且方法体足够简单时,使用
inline
关键字修饰方法
【说明】内联函数在编译期间会被"复制"到调用处,有可能会增大包体积,所以当函数的调用次数很多时,不推荐使用inline
;参数中没有lambda时,如果调用频次过多且方法体很简单,也可酌情考虑使用inline
inline fun <T> complicatedProcessCalledEveryWhere(block: (T)-> Unit){...} // 反例
fun verySimpleProcess(block: () -> Unit){...} // 反例
- 【强制】使用内联函数注意非局部返回
// 反例:由于any是inline的,所以return会将doSth方法整体返回
fun doSth(input: List<String>): Boolean {
input.any { return if (it.isNotEmpty()) true else true }
return false
}
// 正例:添加@any标签,返回any block
fun doSth(input: List<String>): Boolean {
input.any { return@any if (it.isNotEmpty()) true else true }
return false
}
七 集合处理
- 【强制】优先使用工厂方法而不是构造函数构建List、Map等实例
【说明】避免直接创建ArrayList
、LinkedHashMap
,而是使用mutableListOf()
、mutableMapOf()
// 反例:
val list: List<String> = ArrayList<String>().apply { ... }
val mutableList: MutableList<String> = ArrayList()
val map: Map<String, String> = HashMap<String, String>.apply { ... }
// 正例:
val list: List<String> = listOf("1", "2", "3")
val list: List<String> = List(3) { it.toString() }
val mutableList: MutableList<String> = mutableListOf()
val map: Map<String, String> = mapOf("1" to "1", "2" to "2")
- 【强制】优先使用List而不是MutableList、优先创建新的List而不是修改原有MutableList,保证集合的不变性。(同理Set、Map)
【例外】需要注意toMutableList
会创建新的对象,需要在原对象上修改时,使用MutableList
// 反例:
val list = mutableListOf(1, 2, 3)
...
list[1] = 4 //origin list modified
...
// 正例:
val list = listOf(1, 2, 3)
...
val newList = list.toMutableList() // new list created
newList[1] = 4 // new list modified
...
- 【强制】使用操作符重载代替集合原有的
set
/get
操作、使用in
替代contains
// 正例:
list[0]
list[0] = 1
if(0 in list)
// 反例:
list.get(0)
list.set(0, 1)
if(list.contains(0))
八 协程规范
- 【强制】使用
CoroutineScope
必须在适当时机进行cancel
,例如在Activity的onDestroy
中
【说明】GlobalScope
只允许在与Application生命周期相同的类中使用。
// 反例:
class MyActivity: AppCompactActivity(){
override fun onCreate(...){
GlobalScope.launch{...}
}
}
// 正例:
class MyActivity: AppCompactActivity(), CoroutineScope by MainScope() {
//实际项目中,建议让基类Activity实现CoroutineScope,并在onDestroy中统一cancel
//子Activity中创建的协程都成为其子协程,可以自动回收
override fun onCreate(...){
launch{...}
}
override fun onDestroy(){
cancel()
}
}
- 【强制】只有调用了其他
suspend
函数的方法,才允许添加suspend
修饰符
// 反例:以下suspend修饰符无意义
suspend fun parseFile(path: String) = File(path).bufferedReader().readText()
- 【强制】不关心返回值的
suspend
方法(或其他耗时方法),使用launch
而不是async
启动
GlobalScope.async{ doSthInBackground() } // 反例
GlobalScope.launch{ doSthInBackground() } // 正例
- 【强制】
async
用在多个异步调用的结果同步,单个异步调用(相当于仅仅用来切换线程使用)时,使用withContext
// 正例:
launch {
val data = withContext(Dispatchers.Default) { /* code */ }
...
}
// 反例:
launch {
val data = async(Dispatchers.Default) { /* code */ }.await()
...
}
九 Java兼容性
- 【推荐】为Java中的参数/返回/泛型类型添加
@Nullable
/@NotNull
(或@NonNull
)注解,便于Kotlin类型系统解释
@NotNull
Set<@NotNull String> toSet(@NotNull Collection<@NotNull String> elements) { ... }
- 【强制】可以确定Java对象的可空性时,用明确的类型声明对象的引用变量,避免使用可空性未知的平台类型
val s: String? = person.name // 正例
val s: String = person.name // 正例
val s = person.name // 反例:平台类型String! 可空性未知
- 【推荐】需要将Kotlin的属性(普通成员属性、伴生对象属性、对象声明属性)暴露给Java时,为属性添加
@JvmStatic
注解,可以在Java中以属性的方式而非getter的方式访问。请注意不要给常量属性添加@JvmStatic
,这会生成静态方法而非静态变量
// 反例:
class Key(val value: Int) {
companion object {
const val INTEGER_ONE = 1
val BIG_INTEGER_ONE = BigInteger.ONE
}
}
object Util {
@JvmStatic val BIG_INTEGER_TEN = BigInteger.TEN
}
// in java
Key key = new Key(5);
System.out.println(key.getValue());//调用getter
System.out.println(Key.INTEGER_ONE);//访问静态常量
System.out.println(Key.Companion.getBIG_INTEGER_ONE());//通过伴生对象调用getter
System.out.println(Util.getBIG_INTEGER_TEN());//通过静态方法调用
// 正例:
class Key(@JvmField val value: Int) {
companion object {
const val INTEGER_ONE = 1
@JvmField
val BIG_INTEGER_ONE = BigInteger.ONE
}
}
object Util {
@JvmField val BIG_INTEGER_TEN = BigInteger.TEN
}
//in java
Key key = new Key(5);
System.out.println(key.value);//访问字段
System.out.println(Key.INTEGER_ONE);//访问静态常量
System.out.println(Key.BIG_INTEGER_ONE);//访问静态字段
System.out.println(Util.BIG_INTEGER_TEN);//访问静态字段
- 【推荐】需要将Kotlin的函数(伴生对象、对象声明的函数)暴露给Java时,为函数添加
@JvmStatic
注解
// 反例:
class Key(val value: Int) {
companion object {
fun doWork() {}
}
}
object Util {
fun doWork(){}
}
//in java
Key.Companion.doWork();//通过伴生对象调用方法
Util.INSTANCE.doWork();//通过实例调用
// 正例:
class Key(val value: Int) {
companion object {
@JvmStatic fun doWork() {}
}
}
object Util {
@JvmStatic fun doWork(){}
}
//in java
Key.doWork();//调用静态方法
Util.doWork();//调用静态方法
- 【推荐】为有默认参数的函数,添加
@JvmOverloads
注解
// 反例:
class Sample {
fun multiParam(param1: Int = 1, param2: String = "2", param3: Double = 3.0) {
}
}
//in java
Sample().multiParam(1, "2", 3D);//3个参数必须全部指定
// 正例:
class Sample {
@JvmOverloads
fun multiParam(param1: Int = 1, param2: String = "2", param3: Double = 3.0) {
}
}
//in java,可以使用所有重载
Sample().multiParam();
Sample().multiParam(1);
Sample().multiParam(1, "2");
Sample().multiParam(1, "2", 3D);
- 【推荐】有可能抛出异常的方法,需要暴露给Java调用时需要添加
@Throws
注解
【说明】Java会将@Throws
视为throws声明,强制要求try…catch
//kotlin
@Throws(IOException::class)
fun readFile(file: String) : OutputStream = {...}
//java caller
try { //强制要求try...catch
i = readFile("./config.xml")
} catch (IOException e) {
e.printStackTrace();
}
- 【强制】在调用Java的"注册/反注册"的SAM时,需要确保传入实例的唯一性,不允许通过lambda创建实例对象,否则无法保证传递的是同一个实例。
//defined in java
public class Widget {
public interface Listener {
void onEvent(@NotNull Widget widget);
}
public int getListenerCount() { ... }
public void addListener(@NotNull Listener listener) { ... }
public void removeListener(@NotNull Listener listener) { ... }
}
// 反例1:匿名内部类,每次创建新的实例
val widget = Widget()
widget.addListener{ widget: Widget -> println("Listened to $widget") }
println(widget.listenerCount) // will print 1
...
widget.removeListener{ widget: Widget -> println("Listened to $widget") }
println(widget.listenerCount) // still print 1
// 反例2:直接使用lambda,每次被包装成新的实例
val widget = Widget()
//listener type is Function1 when using lambda expression
val listener = { widget: Widget -> println("Listened to $widget") }
widget.addListener(listener) //listener will be wrapped into Listener type
println(widget.listenerCount) // will print 1
...
widget.removeListener(listener) // listener will be wrapped into another instance
println(widget.listenerCount) // still print 1
// 正例:使用SAM构造器,或直接使用object 表达式,实例相同
val widget = Widget()
//listener type is Widget.Listener when using SAM constructor
val listener = Widget.Listener { widget: Widget ->
println("Listened to $widget")
}
widget.addListener(listener) //no wrapping occurs
println(widget.listenerCount) // will print 1
...
widget.removeListener(listener) // no wrapping occurs
println(widget.listenerCount) // still print 0
十 Android开发规范
- 【推荐】启动Activiy推荐通过伴生对象实现静态方法,减少key的暴露
class MyActivity{
companion object{
private const val USER_NAME: String = "user_name"
@JvmStatic fun startActivity(userName: String){...}
}
}
- 【推荐】使用
@Parcelize
注解简化Parcelable类
【说明】不需要为新添加一个属性整体修改Parcelable相关的read/write方法。
@Parcelize
data class User(val id: String, val name: String, ...): Parcelable
- 【强制】Activity、Fragment中不使用
findViewById
获取跟布局的子View,使用kotlin-android-extension插件替代,View的id命名推荐使用驼峰命名而不是下划线
标签:正例,...,BAT,val,编码,Kotlin,反例,fun,强制 来源: https://blog.csdn.net/vitaviva/article/details/104441660