背景
初次接触 Kotlin 也比较晚,是在 Google IO 大会上,听到说将作为官方支持的 Android 开发语言,就开始尝试用 Kotlin 写一些项目,很快便被它简洁精炼的语法所迷住,当然对于熟悉 Scala 等语言的人来说可能并没有那么神奇,但是对于一直使用 Java 写代码的我来说,用完 Kotlin 之后简直是不想再写 Java 了。
在真正熟悉 Kotlin 后,我便开始着手将公司的项目向 kotlin 转型(恰好公司的项目也需要代码重构),于是所有的新模块、重构的模块也都开始使用 Kotlin 了,到目前为止使用 Kotlin 也差不多有半年多了,在开发过程中也总结了一些相关的开发经验,这次就跟大家分享一下这些内容吧。
经验总结
一. 巧读 Kotlin 语法糖
当我们想知道 Kotlin 代码内的一些语法糖是如何实现的时候,我们该怎么做呢?因为 Kotlin 也是基于 JVM 的语言,所以我们可以先将其编译为 class 文件,然后再将 class 文件反编译为 Java,这样我们就可以参照 Java 的实现内容来理解 Kotlin 的一些语法糖了(这里假设大家都会 Java 哈,毕竟学习 JVM 相关的语言,Java 基本上是必须要会的)。虽然也有一些其他的理解 Kotlin 实现的方法,但是我觉的这个方法是最小白也是最简单直接的了。
下面通过在使用类代理时遇到的一个有意思的问题来给大家简单演示一下,大家可以猜一下下面这段代码的输出内容是啥:
interface IPerson {
fun name(): String
}
class Person(private val mName: String): IPerson {
override fun name() = mName
}
class PersonProxy(private var mPerson: IPerson): IPerson by mPerson {
fun changePerson(person: IPerson) {
mPerson = person
}
}
fun main(vararg args: String) {
val proxy = PersonProxy(Person("person1"))
println(proxy.name())
proxy.changePerson(Person("person2"))
println(proxy.name())
}
答案是依次输出『person1』和『person2』吗?当然不是,连续输出了两次 『perosn1』,为什么呢?我们通过上述方法反编译为 Java 文件看下实现原理:
public final class PersonProxy implements IPerson {
private IPerson mPerson;
@NotNull
public String name() {
return $$delegate_0.name();
}
public PersonProxy(@NotNull IPerson mPerson) {
$$delegate_0 = mPerson;
this.mPerson = mPerson;
}
public final void changePerson(@NotNull IPerson person) {
kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull(person, "person");
mPerson = person;
}
}
我们可以看到,Kotlin 为了保证类代理的安全使用,用一个特殊的变量将需要代理的类记录下来,所以当你调用 changePerson() 函数时,代理类的 name() 输出并不会改变。
二. 谨慎使用 lateinit 关键字
我们知道在 Kotlin 中,『null』 被当做了单独的类型,对于可能为空的变量,需要在定义时特殊标识出来,在使用是也需要用 『?』 或者『 !!』 进行标记,代码类似于下方:
var mMsg: Message? = null
fun getMessageCode() = mMsg?.code ?: Message.INVALID_CODE
可能有的同学在使用时觉的,为啥还要多写个 『?』 啊?使用起来一点也没有 Java 简单,但是好像用 『lateinit』 标识 mMsg 后就可以不用加 『?』 了,赶紧加一下!
但是这样做就违背了 Kotlin 的设计初衷了,本身把『null』当做特殊类型就是为了屏蔽 Java 的空指针问题,当你把变量用『?』标识后,编译器会提醒你这个变量可能为空,并强制你做变量为空时的特殊处理,这样就可以大大减少空指针问题的发生机率,但是你用『lateinit』标识后,编译器就不太会帮助你检查代码了。
那『lateinit』何时使用呢?我们可以保证一个变量的初始化一定在其他所有代码使用它之前时,我们就可以用『lateinit』关键字(如果你不能保证就一定要用『?』标识变量类型或者用下面介绍的方法),如下方代码:
class App : Application()
companion object {
@SuppressLint("StaticFieldLeak")
lateinit var INSTANCE: Context
}
override fun onCreate() {
super.onCreate()
INSTANCE = this
}
}
如果我还是不想用『?』标识变量,这个变量也可能为空,我该怎么做呢?那我们我们可以借鉴在 Java 中常使用的特殊变量法来解决,就是用一个特殊的变量来替代 『null』,方法类似下方代码:
data class Message(val code: Int, val detail: String) {
companion object {
const val INVALID_CODE = -1
const val INVALID_DETAIL = EMPTY_STR
val INVALID_MSG = Message(INVALID_CODE, INVALID_DETAIL)
}
fun isValid() = this != INVALID_MSG
}
var mMsg = Message.INVALID_MSG
三. 巧用 get/set 方法
在类的开闭设计原则中,我们一般不会向外暴露一些不必要的内容,考虑一个情景,如果一个变量,我们需要对外暴露它并不希望使用者改变他,但是这个变量会在类的内容不被改变,我们会怎么设计呢?用 Java 我们可能会这么做:
public final class MessageCenter {
private int mMsgCount = 0;
int getMsgCount() {
return mMsgCount;
}
void saveMsg(final Message msg) {
mMsgCount++
//...
}
}
代码看起来还行,但是看看 Kotlin 下我们可以怎么做呢?
class MessageCenter {
var mMsgCount = 0
private set
fun saveMsg(final Message msg) {
mMsgCount++
//...
}
}
是不是简洁了一些,当然这只是一个简单的例子,更多的妙用大家可以自行发掘!
四. 谨慎使用 Lamba 表达式中的 it 与变量名省略
大家可以先考虑下当你维护下面这段代码的时候:
AlertDialog.Builder(this)
.setPositiveButton(R.string.sure) { _, _ ->
sureCallback()
}
mDisplayer.confirmTagName(mDisplayer.getClipPhoto(tag.path), tag.name) {
mModel.changeTagForCurTask(tag, it) {
refreshDisplayer()
}
}
what?! 这里面 『_』、『it』都代表的是什么鬼?代码读不懂啊?只能一层一层的往上找接口的定义。
虽然说写代码的时候这样写很方便,但是对于后续的代码维护实在是不够友好,对于一些十分明显的接口,如果集合的 『foreach』 接口,我们可以放心大胆的用 『it』, 但对于一些指代不明的接口,我认为还是把完整的参数名写清楚较好(即使编译器提醒你修改,当然最好把这方面的提醒关掉),毕竟我们写代码也要考虑后续的维护工作,上面的代码修改后如下:
AlertDialog.Builder(this)
.setPositiveButton(R.string.sure) { dialog, which ->
sureCallback()
}
mDisplayer.confirmTagName(mDisplayer.getClipPhoto(tag.path), tag.name) { tagName ->
mModel.changeTagForCurTask(tag, tagName) {
refreshDisplayer()
}
}
是不是容易读懂了些?记得公司的测试同学对一位代码风格很好的同学的评价就是,他写的代码即使逻辑复杂,但是从头读下来,很容易就读懂了,就像像读文章一样顺畅(当然也要有合适的注释哈)。
五. 善用扩展属性与扩展函数
扩展属性很容易理解也很好理解其用途,给一个类直接添加一个属性,
但是扩展函数的作用是什么呢?为什么扩展函数会诞生?扩展函数和我们平常使用的工具函数有什么不同呢?并且当我门在 Java 中调用 Kotlin 中的扩展函数时我们会发现其调用方式就是通过工具函数的方式调用的,那我们为啥还要用扩展函数呢?下面我就就我自己的理解来简单解释一下。
// 扩展函数
fun Message.isUsable() = code >= 0
// 工具函数
fun isUsable(Message msg) = msg.code >= 0
上面的代码简单展示了扩展函数与工具函数,大家可以简单回忆一下自己在写代码的时候查看一个对象有哪些可以调用的函数时是怎么操作的吗?我们先写下对象,然后敲出『.』,编辑器就自动帮我们把可用的函数搜寻并展示出来了,此时扩展函数也会展示出来,这样我们就可以知道已经有了这样的扩展函数了或是同事已经写了这样的扩展函数了,我们就不用重复写了,但是编辑器却没有帮我们搜寻工具函数的功能,也就是说我们可能会写很多功能相同的工具函数,因为我们不会知道有没有人写过,这样就会有大量的重复代码(相信大家都有过项目里很多重复工具函数的体验),所以在我理解来扩展函数较之工具函数有如此的好处,能用扩展函数的地方我们就应该尽量避免使用工具函数。
当然扩展函数还有其他的比较方便的作用,可以直接进入对象的,类似 with、apply、use 等非常有意思的函数都是通过扩展函数来实现的,实现方式都大概类似如下函数:
inline fun <T> T?.notNull(action: T.() -> Unit) {
if (this != null) {
action()
}
}
// 获取最新非空信息时 toast 出信息详情
MessageCenter.getNewMsg().notNull {
toast(detail)
}
六. 变长参数的传递
fun printLength(vararg args: Any) {
println(args.size)
}
fun testFun(vararg args: Any) {
printLength(args)
}
fun main(vararg args: String) {
testFun(1, 2, 3)
}
大家可以猜测一下上面的程序输出是多少?是 3 吗?当然不是,结果是 1 ,为什么呢?大家可以用一中分享的方法自己反编译一下,很容易理解的,那我们如果想让程序输出 3 该怎么做呢?只需要将 testFun()修改为如下代码即可:
fun testFun(vararg args: Any) {
printLength(*args)
}
大家可以再深入研究一下当把变量的类型从 Any 改为特定类型(比如 Int、Float、String 等)时会怎样,这个地方还是很有意思的。
总结
语言说来只是工具,重要的还是编程的思想。这次就暂且先写这些,还有一些其他的总结下次有时间再继续分享下~