Kotlin 使用经验杂谈(一)

背景

初次接触 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 等)时会怎样,这个地方还是很有意思的。

总结

语言说来只是工具,重要的还是编程的思想。这次就暂且先写这些,还有一些其他的总结下次有时间再继续分享下~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容