向BAT看齐!某大厂内部的Kotlin编码规范

某BAT大厂内部使用的Kotlin编码规范,在Kotlin官方规范的基础上又补充了不少内容,很有参考价值。

一 代码组织

  1. 【强制】在混合Java源码项目中,Kotlin 源文件应当与 Java 源文件位于同一源文件根目录下, 无需按照文件类型分开放置

  2. 【强制】如果源文件中只包含单个类,则以这个类名作为该文件名

  3. 【强制】一个类的内容按以下顺序排列(PS:官方推荐伴生对象放到最尾,但是源码里也有放到顶部的case,但是不要放在中间)

    - 属性声明与初始化块 
    - 次构造函数 
    - 方法声明 
    - 伴生对象
    
  4. 【强制】在实现一个接口时,实现成员的顺序应该与该接口的成员顺序相同

二 命名规范

  1. 【强制】包的名称使用小写且不允许出现下划线
import com.intellij.openapi.action_system // 反例
  1. 【强制】包名点分隔符之间的名称尽量简短,避免使用多个单词的名称,若确实需要使用多个单词,则将小写字母连接在一起,避免使用驼峰命名
import org.example.myproject // 正例
import org.example.myProject // 反例
  1. 【强制】对象声明(单例类)的名称和普通类一样,以大写字母开头并使用驼峰
object Comparator {...}
  1. 【强制】泛型类型用全大写字母表示
// 反例:泛型没有全大写
class Box<Type>(t: Type) {
    var value = t
}
  1. 【强制】常量使用大写、下划线分隔的名称,力求语义清晰,不要嫌名字长
    【说明】常量包括 const 的属性,或者顶层val 属性Type
    【例外】有行为或者有custom getter的对象除外
const val MAX_COUNT = 8
val USER_NAME_MAP = mapOf("UserName", ...)
  1. 【强制】枚举常量使用大写、下划线分隔的名称
enum class Color { RED, GREEN }

三 代码格式

  1. 【强制】类/接口如果没有body内容,则省略花括号
class EmptyClass {} // 反例
interface EmptyInterface {} // 反例
object EmptyObject {} // 反例
  1. 【强制】对于由单个表达式构成的函数体,优先使用表达式形式。
fun foo() = 1 // 正例
fun foo(): Int { return 1 } // 反例
  1. 【强制】自定义 get 与 set方法如果包含代码块,需要将get、set、换行写
// 简单的get不许换行
val isEmpty: Boolean get() = size == 0

// get有代码块 换行写
val foo: String
    get() { /*...*/ }
  1. 【强制】when 语句中 将短分支放在与条件相同的行上,无需花括号。
// 正例:
when (foo) {
    true -> bar()
    false -> baz()
}

// 反例:
when (foo) {
    true -> { bar() }
    false -> { baz() }
}
  1. 【强制】定义方法时,如果有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 

四 惯用语法

  1. 【强制】声明变量时默认使用非空类型

  2. 【强制】能保证在使用时一定非空的成员,使用lateinit而不是可空类型。
    【说明】注意Activity恢复重建等可能会影响初始化的流程,此时也应该保证lateinit变量在使用时非空

  3. 【强制】尽可能使用val而不是var

  4. 【强制】尽可能减少类、成员的open声明

  5. 【强制】尽可能使用List/Set/Map,而不是MutableList/MutableSet/MutableMap

  6. 【强制】连接多个变量时,使用字符串模板

val fullName = "${user.firstName} ${user.lastName}" //正例
val fullName = user.firstName + " " + user.lastName // 反例
  1. 【强制】单行函数省略大括号,返回值类型明确的情况下也可省略
fun foo(a: Int, b: Int) = a + b // 正例
fun foo(a: Int, b: Int): Int { return a + b } // 反例
  1. 【强制】调用函数的最后一个参数是函数类型时,省略圆括号
observable.subscribe({  /*...*/ }) // 反例
observable.subscribe { /*...*/  } // 正例
  1. 【强制】使用==替代equals
if (this::class.java.equals(clazz)) // 反例
if (this::class.java == clazz) // 正例
  1. 【强制】在使用已经做过类型检查的变量时无需做类型转换
if (x is String) { print(x.length) } // 正例
if (x is String) { print((x as String).length) } // 反例
  1. 【强制】通过as?进行运行时类型检查
println((s as? Int) ?: 0) // 正例
if (s is Int) println(s) else println(0) // 反例
  1. 【强制】when语句结尾需要加else,以防出现逻辑错误
    【例外】密封类只需要列举所有子类

  2. 【强制】利用表达式语句,避免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
    }
}
  1. 【强制】for循环中使用until替代size - 1
// 反例:
for (i in 0..(list.size - 1)) {
    ...
}

// 正例:
for (i in 0 until list.size) {
    ...
}
  1. 【强制】连续对同一个变量进行读/写操作时,使用作用域函数
    【说明】连续访问可空变量需要多次?.每次都会进行判空。但使用?.+作用域函数时,作用域内部引用的都是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{ … }--
  1. 【强制】对于初始化成本较高的变量,使用lazy进行惰性初始化;初始化成本不高则避免使用
    【说明】滥用lazy会创建额外对象,增加性能开销
// 反例:创建空list对象的成本不高,不要使用lazy
val list by lazy { emptyList<Any>() }
  1. 【强制】lazy代理默认是线程安全的,单线程环境使用LazyThreadSafeMode.NONE提高性能
val lazyString: String by lazy(LazyThreadSafetyMode.NONE) { }
  1. 【强制】需要声明基本类型数组时,优先使用原生类型数组,而不要使用泛型数组
    【说明】向泛型数组中添加基本类型会产生额外装箱开销
fun function(array: IntArray) { }  // 正例
fun function(array: Array<Int>) { } // 反例
  1. 【强制】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

五 类与对象

  1. 【强制】不对外部模块公开的非私有类、方法以及属性,需要添加internal修饰符

  2. 【强制】除非内部类需要访问外部类成员,否则不要为该类添加使用inner修饰符

  3. 【强制】open的子类中,override的成员默认是open的,如果确定其后续不应该再被继承,必须添加final修饰符

open class Foo() : Bar() {
    final override fun test() { // 不需要子类重写,需添加final
        super.test()    
    } 
  1. 【强制】数据类中不允许有var属性。使用数据类的copy方法进行属性的变更。
// 反例:数据类应该使用val属性
data class MutableDataClass(var i: Int) {
    var s: String? = null
}
  1. 【强制】伴生对象中定义常量,需要加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"
}
  1. 【强制】使用object声明代替只有companion object的类(本类或者父类里没有其他类成员)
// 反例:
class MyClass{
    companion object{
        fun doSth(){...}
    }
}

// 正例:
object MyClass{
    fun doSth(){...}
}

六 函数与lambda表达式

  1. 【推荐】调用多参数函数时,尽量使用命名参数提高代码的可读性
//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, '_')
  1. 【推荐】优先使用顶层扩展函数替代静态工具类
// 反例:工具类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> { ... }
  1. 【推荐】代码中需要多次使用的函数类型,可以定一个类型别名。注意为参数添加命名
typealias MouseClickHandler = (payload: Any, event: MouseEvent) -> Unit
  1. 【推荐】声明函数类型时,不要省略变量名,有利于调用处的IDE的代码补全
// 正例:
fun performRequest (
    url: String,
    callback: (code: Int, cotentL String) -> Unit
) { ... }

// 反例:
fun performRequest (
    url: String,
    callback: (Int, String) -> Unit
) { ... }
  1. 【推荐】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
}
  1. 【强制】参数包含lambda且方法体足够简单时,使用inline关键字修饰方法
    【说明】内联函数在编译期间会被"复制"到调用处,有可能会增大包体积,所以当函数的调用次数很多时,不推荐使用inline;参数中没有lambda时,如果调用频次过多且方法体很简单,也可酌情考虑使用inline
inline fun <T> complicatedProcessCalledEveryWhere(block: (T)-> Unit){...} // 反例
fun verySimpleProcess(block: () -> Unit){...} // 反例
  1. 【强制】使用内联函数注意非局部返回
// 反例:由于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
}

七 集合处理

  1. 【强制】优先使用工厂方法而不是构造函数构建List、Map等实例
    【说明】避免直接创建ArrayListLinkedHashMap,而是使用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")
  1. 【强制】优先使用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
...
  1. 【强制】使用操作符重载代替集合原有的set/get操作、使用in替代contains
// 正例:
list[0]
list[0] = 1 
if(0 in list)
// 反例:
list.get(0)
list.set(0, 1)
if(list.contains(0))

八 协程规范

  1. 【强制】使用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()
    }
}
  1. 【强制】只有调用了其他suspend函数的方法,才允许添加suspend修饰符
// 反例:以下suspend修饰符无意义
  suspend fun parseFile(path: String) = File(path).bufferedReader().readText()
  1. 【强制】不关心返回值的suspend方法(或其他耗时方法),使用launch而不是async启动
GlobalScope.async{ doSthInBackground() } // 反例
GlobalScope.launch{ doSthInBackground() } // 正例
  1. 【强制】async用在多个异步调用的结果同步,单个异步调用(相当于仅仅用来切换线程使用)时,使用withContext
// 正例:
launch {
    val data = withContext(Dispatchers.Default) { /* code */ }
    ...
}

// 反例:
launch {
    val data = async(Dispatchers.Default) { /* code */ }.await()
    ...
}            

九 Java兼容性

  1. 【推荐】为Java中的参数/返回/泛型类型添加@Nullable/@NotNull(或@NonNull)注解,便于Kotlin类型系统解释
@NotNull
Set<@NotNull String> toSet(@NotNull Collection<@NotNull String> elements) { ... }
  1. 【强制】可以确定Java对象的可空性时,用明确的类型声明对象的引用变量,避免使用可空性未知的平台类型
val s: String? = person.name // 正例
val s: String = person.name // 正例
val s = person.name // 反例:平台类型String! 可空性未知
  1. 【推荐】需要将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);//访问静态字段
  1. 【推荐】需要将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();//调用静态方法
  1. 【推荐】为有默认参数的函数,添加@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);
  1. 【推荐】有可能抛出异常的方法,需要暴露给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();
}
  1. 【强制】在调用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开发规范

  1. 【推荐】启动Activiy推荐通过伴生对象实现静态方法,减少key的暴露
class MyActivity{
    companion object{
        private const val USER_NAME: String = "user_name"
        @JvmStatic fun startActivity(userName: String){...}
    }
}
  1. 【推荐】使用@Parcelize注解简化Parcelable类
    【说明】不需要为新添加一个属性整体修改Parcelable相关的read/write方法。
@Parcelize
data class User(val id: String, val name: String, ...): Parcelable
  1. 【强制】Activity、Fragment中不使用findViewById获取跟布局的子View,使用kotlin-android-extension插件替代,View的id命名推荐使用驼峰命名而不是下划线
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fundroid

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值