目录

半天入门了下 kotlin

最近好多人都在用 kotlin 了,我真实接触并且使用 kotlin 的时候是 2019 年。那个时候也依葫芦画瓢写了一篇《lateinit 和 by lazy 》的文章

但是,当时写代码还是停留在 if-else、 for 循环等等,跟 java 代码没什么区别,所以并没有深入的理解。

我目前是带着目的去看别人写的 kotlin 代码,我也不想一瞬间就能看懂别人写的 kotlin 代码,毕竟有很多诡异的写法,再加上每个人习惯不同,所以我只要求自己能看懂 80%,剩下的遇到的再去查。我也不要求自己能写的很好,目前只是能看懂,能改改,那就足够了。

超快从 Java -> Kotlin

首先 if-else、for 循环这些没什么区别,多了个 range,这个也不难理解,switch-case 变成了 when。业务代码大多是这种东西的堆叠,所以只要能看懂这个,就能看懂 kotlin 代码的 40%-50%。

其次,一些变量的修饰符,我觉得这个是我一开始很头疼的东西。

都说 var 是可变的, val 是不可变的,且均要进行初始化 即使赋值是 null,这个地方就关联了很多诡异的写法。

类成员变量初始化的各种写法

最基本的概念

var someObj: SomeObject = SomeObject(1)
someObj = SomeObject(2)

val finalObj: SomeObject = SomeObject(1)
finalObj = SomeObject(2) // 编译报错

基本概念就是 var 修饰的变量后续 可变,val 不可变。先这么记住,后续会存在 打脸。 你会发现我这里声明的变量都初始化了,不初始化行不行?

// 注意这里的都是类成员变量,不是局部变量
var someObj: SomeObject // 报错
var someObj2: someObject = null // 报错
val finalObj: SomeObject // 报错

因为 kotlin 为了避免空指针,类成员变量不允许不初始化 (后续也会打脸),一定要初始化(你初始化为 null 也行,但是还会检测出你复制为 null 了,要求你转为 可空类型)。

有可空类型,就有非空类型

  • 可空类型

    顾名思义,可能是空的类型。kotlin 在访问一个类的成员变量时,其实是访问的是其 get set 方法,他会去判空。可空类型在kotlin中的写法就是带个问号

    var someObj2: someObject = null // 报错
    var someObj3: someObject? = null // ok
    
  • 非空类型

    更简单了,不是空~~

打脸成员变量一定要初始化

看这段代码

lateinit var someObj: SomeObject

这里就是声明了一个叫做 someObj 的成员变量,类型是 SomeObject,声明变量时可以在冒号后指定类型,也可以不指定。这都不是重点。重点是,不是说一定要 初始化么 ?好了,这里没有初始化,打脸了,但是看到一个关键字,lateinit 来了。

什么是 lateinit ?这是我 2019 年碰 kotlin 的时候,就了解了的。

想追原理,自己去看字节码。

总结一句话就是,我就是想打破 kotlin 的赋值初始化规则,让编译器不要管我有没有初始化了(为什么编译器要管初始化的问题?因为 kotlin 想尽力避免空指针的出现,所以都要初始化),我自己去负责这个变量的初始化。

那么问题来了,如果你用 lateinit 声明了一个变量,让编译器放你一马,之后你又没有去管理这个变量的初始化,在之后的代码里没有对它进行任何的赋值操作,那么你自己在运行时使用这个变量的时候,很显然啊,空指针啊!

那么问题又来了,既然 lateinit 只是说明我这个属性是暂时不初始化的,那我为什么不能直接用 var + null 声明呢?如下

var somObj: SomeObject = null

这样写对吗?铁定不对啊,编译器会给你报错 Null can not be a value of a non-null type SomeObject,为啥?前面基本概念就说了,你这是可空类型,得加问号啊。但是如果你用 lateinit,就不需要加这个烦人的问号。

使用 var + null 初始化的可空类型的后续问题

一旦你的一个成员变量是可空类型,你在后续写代码的时候使用会有各种烦人的问题,比如下面这一坨代码。

private fun getLayoutView(): View? {
    if (mLithoView == null) {
      mLithoView = LithoView(rootView.context)
    }
    return mLithoView
}

var mPlcEntryDataAdapter: PlcEntryDataAdapter? = null

if (getLayoutView() != null) {
    mStyleView = FlexStyleUtils.addStyleViewToParent(
        rootView as RelativeLayout,
        getLayoutView()!!,
        getContainerId())
}

if (mPlcEntryDataAdapter?.actionType == PlcEntryStyleInfo.ActionType.APP_DOWNLOAD) {
    mDownloadActionObserver.onNext(false)
}

你会发现有很多的 ?????,居然还有 !!!!
是的没错,为了尽可能避免空指针,kotlin 要求所有成员变量都要初始化对吧,你居然还想初始化赋值为 null,那我 kotlin 辛辛苦苦想避免的空指针不就白费了么?所以,如果你一定要这么赋值,那么你就要在声明中加个问号,使用时也得带着问号,就是所谓的可空类型嘛,也就是上面代码里的 mPlcEntryDataAdapter?和 mPlcEntryDataAdapter?.actionType 这个变量的声明和使用的体现。

!!这个又是什么呢?

为什么他的 kotlin 代码里有 !!

!! 的作用其实就是告诉编译器,我这个可空类型的变量是非空的!(真打脸)

但是,这在 android 中是非常常见的。比如我们经常使用的 intent 来传递参数,可是 intent 是可空类型的,那么我们从 intent 里面读出来的参数,如果使用 var 声明,那么肯定也是可空类型的。但是,我们又知道,这个 intent 肯定不是空的,我们会传自己的参数进来,所以我们存在 intent 里面的参数也一定不是空的,我们接收参数的方法,也是接收一个非空参数。这个时候只能如下这样写用 !!,或者 lateinit。

var config: AlbumDetectionConfig? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    config = intent?.let {
        IntentUtils.getSerializableExtra<AlbumDetectionConfig>(
            it,
            AlbumDetectInitModuleALBUM_DETECTION_CONFIG_KEY
            )}?:
            emitter.onError(IllegalArgumentException("AlbumDetectionConfig must not be null!!"))
    VideoInfo.buildVideoInfo(config!!, count++)
}

这里因为 intent 可空,所以 config 可空,又因为 buildVideoInfo 接收非空类型,所以我们只能用 !!,或者 lateinit,但是 lateinit 只能用于声明全局变量,很多时候我们只需要一个局部变量,所以很多时候只能使用 var 和 !!

在上面的代码里,我们又会发现了两个诡异的东西 let?:

打脸 val 和 final

终于搞完了前面一大堆的 var 相关的类成员变量声明的问题,这时候我们一直谨记着 var 可变 val 不可变。

但是你又会看到这样的代码

val size: Int
    get() {
      return value?.size() ?: -1
    }

首先看这个写法,咋还能这么写???是的,他就是可以这么写,可以重写 get 和 set 方法。

然后看 size 这个成员变量,这个成员变量是 val 声明的,但是···这不是随着 value 的值在变么?value 空了就是 -1,不是空就是 value 的大小。一直以为 val 声明的就是 final 的变量,其实不然···

这其实是因为 kotlin 为我们生成了所有类成员变量的默认的 get set 方法,我们当然可以重写,我们声明的 val 只是声明了一个 final 的 get 方法仅此而已,而不是声明了一个 final 的成员变量,这个可以通过转 java 代码查看。

好了,至此,搞懂这些,应该能看懂 60%-70% 的 kotlin 代码了吧~~

烦人的各种 Scope Function

kotlin 比 java 多出了很多这些个东西,会阻断你的学习,让你觉得学起来很烦,不想学了。其实,到这里也就差不多了,知道了 let?:,知道了 by or by lazy,基本就差不多了。

当你查 let 的时候,你就会发现原来还有 with、run、apply、also,让你想哭,但是这些都是功能不同罢了。

let 经常使用在上面代码中的场景下:可空类型不为空才会执行 let 的函数体,又因为 lambda 可以省略 return,所以上面的那一坨代码的意思是,intent 空的话那我就不执行 let 里面的函数体了,那就是个 null 了。?: 的意思就是,如果前面的是空则执行后面的代码。那么就是说,如果 intent 空了或者 intentUtils.getSerializableExtra() 的结果是空了,就执行 emitter.onError(),就是这么简单。

by 关键字是一块很多的东西,但是你看字节码或者转 java 代码看的时候就知道,这其实就是个委托的概念。

类和方法

这里 kotlin 也有很多新概念,你会发现你甚至没法继承类了,没法重写方法了。没错,因为一定要加 open 关键字

还有许许多多的概念,比如什么是伴生对象?什么是密封类?什么又是内联类?什么是幕后字段????????

这里只说几个常用的

伴生对象

首先 object 在 kotlin 中也是一个关键字,它可以用于

  • 对象声明
    懒加载的单例

  • 伴生对象
    也是我们经常看到的 companion object xxx 写法

  • 匿名内部类
    android 中也很常见 object: interface {}
    而且可以随意增加方法,外部还能继续调用,而 java 中在匿名内部类声明的方法,外部是无法访问匿名内部类里新声明的属性或者方法的。

主要来看伴生对象,如果想快速理解的话就是伴生对象在 kotlin 中的感觉就像是 java 中的 static 一样,因为 kotlin 中没有 static 的概念。

companion object 是通过创建一个单例类来实现的。

如果你只是在 kotlin 代码中互相调用,是没有感知的,因为 kotlin 中 object 的声明,就代表了可以直接引用。

object A {
    var a = "A"
    fun a() {}
}

fun main(args: Array<String>) {
    A.a // ok
    A.a() // ok
}

这就跟 java 中的静态方法调用写法一致,但是实际 A 是个单例类,当你从 java 代码中调用时会明显发现不对劲。

public static void main(String[] args) {
  A.a(); // error
  A.INSTANCE.a(); // ok
}

这里就很明显了吧,其实就是一个单例类而已。而 companion object 同理,在 java 中调用时要用 A.Companion 来调用。

这里就有了另一个注解 @Jvmtatic,如果你加上了这个注解,那么编译器会在外部帮你生成一个真实的 java static 方法或者成员变量,那么你在 java 代码中调用的时候,就不需要 A.Companion 了,可以直接 A.a()!

数据类和密封类

数据类就是 data 关键字来声明,只是帮我们生成了一些常用的方法仅此而已。
密封类其实也没啥,是个抽象类,不过被严格要求了子类声明位置(所以密封了嘛···),这样可以对集成类型进行限制,算是一种代码设计层次的东西。

getter/setter 和 幕后字段

java 中我们要写 getter 和 setter,kotlin 中对于类型成员变量的方法和改变也是 getter 和 setter。
但是下面这段在 java 中看似没毛病的代码确是不行的~~

class A {
  var a = ""
      set(value) {
          this.a = value
      }
}

运行直接栈溢出

所以为了解决这个问题出现了 幕后字段,用关键字 field 表示,我基本没用到过,所以刚才的代码要这么写

class A {
  var a = ""
      set(value) {
          field = value
      }
}

之后我看到了一些 幕后属性 的概念,但是没看到官方对此有任何的说明,举例也没有任何关键字,只是一种约定,感觉像是自己说出来的东西。
通过 _a,这种变量名写法表示是 幕后属性?,然后再声明个 a,外部访问只能访问 a,不可访问 _a, a 的值依赖 _a,来达到一种可读不可写的作用域。

但是,直接声明个 a,private set,外部不就不可以调用了吗?内部依旧可以

协程

因为之前写过一段时间 go,go 也有 Goroutines,相类似的概念,但是 golang 可是自己写了一波调度器的,那 kotlin 如何实现的呢?

未完待续···


2021.1.10 补充

看了一波别人写的实现原理分析,源码也看了点,发现 kotlin 其实也是做了一套调度,帮忙更好的处理异步操作。

那既然如此,只能去慢慢学 api 了,没有办法的。

而且配上 kotlin 之后,加上 Android Architecture Components 那一套东西,帮你处理了很多生命周期和对象释放的问题等等···会让代码看起来十分简洁,不知道这算是对编程人员的要求更低了,还是要求更高了呢?

关于协程能否提升性能?这个看你怎么看 协程 这两个字了,如果你说的是 Goroutines,那确实可以提高性能,如果你说的是 kotlin 中的协程,不能,就是线程池而已。

为什么有这些区别?主要在于协程的实现上的区别,其实本质的纯洁的协程和性能没有半毛钱关系,它只是一段可供调用的逻辑段,谈何性能呢?

只不过,配合上线程,那么就能做到很多调度(也要看你怎么实现,但是这部分本身已经超出了最本质的协程的概念),写出同步形式的异步代码。