Kotin基础

243 阅读57分钟
  • Kotlin相关的知识该有的都在这了。
  • 做这个笔记也是为了方便自己用的时候搜索和复习。

1.变量、函数和类型

声明变量

  • 初始化需要初始值
    • 首先 var view:View报错
    • kotlin变量没有默认值
    • var view:View = null也不行
    • var view:View = findViewById(R.id.tv)也不行,findViewById现在不能用,必须写在onCreate里面或之后
    • 到底怎么初始化? 需要先了解空安全设计

空安全设计

  • Kotlin会通过IDE的报错,来避免null对象的调用,从而避免空指针
    • 就是不想先初始化怎么办?-->延迟初始化
      • “我很确定我用的时候绝对不为空,但第一时间我没法赋值”的场景
    • lateinit var view: View
      • lateinit:让IDE不要检查报错
    • 解除非空限制
      • 有时候从服务器去一个JSON数据,并解析成User对象,服务器给的数据未必有name,这个时候,空值就是有意义的。对于可以为空值的对象,可以在类型右边加?解除非空限制
      • class User{ var name: String? = null }
      • 除了可以设置为空值,在代码使用的任何地方也可以
        • var user = ...
        • user.name = null
      • 空类型的变量会有新的问题
        • 由于对空引用的调用会导致空指针异常,所以Kotlin在可空变量直接调用的时候IDE会报错
        • 可能为空的变量,Kotlin不允许用
          • 那怎么办?用之前检查一下
          • if(name != null){ println(name.length) }
            • 还是报错?因为多线程情况下,其他线程可能在你检查后再把它改为空。Kotlin里面不是这么玩的,而是用?
          • println(name?.length)
            • 这个写法同样会对变量做一次非空确认之后再调用方法
            • 这个是Kotlin的写法,它可以做到线程安全
          • 另外还有一种双感叹号的写法
            • println(name!!.length)
              • 告诉编译期:我保证view是非空的
              • 断言
              • 这样基本和java没有区别了,同时也享受不到Kotlin的空安全设计

类型推断

  • 在声明的时候就赋值,不写变量类型也可以
var a = "haha"
println(a) //输出:haha
a = 1 //报错,类型不兼容
  • 代码不用写变量类型,编译器编译的时候会帮忙补上

变量类型

  • var
    • variable的缩写
  • val
    • 只读变量
    • 只能赋值一次,不能修改
    • value的缩写
    • 在java里设置只读变量是用的final关键字

函数声明

  • java
    • Food cook(String name){}
  • kotlin
    • fun cook(name: String): Food{}
  • 两者没有区别
  • 函数的参数也有可空的控制--> (name: String?)
  • 函数没有返回值
    • java里是void

    • Kotlin是Unit,可以省略fun shout(): Unit{}

函数有默认的getter、setter支持

  • 当你取值、赋值的时候,实际调用的是get和set方法
    • name = ”Kotlin“//你写的代码
    • setName("Kotlin")//实际上发生的)
    • println(name)//你写的代码
    • println(getName())//实际上发生的代码
  • 这样的作用:
    • 所有属性的读写,都自动加了一个钩子,你可以为每个变量设置钩子代码
var name: String = "Kotlin"
fun setName(newName: String){
  name = newName
  log("new name set")
}
    • 另外你也可以用这个钩子去修改变量的赋值和取值的具体手段
val dataValid: Data
fun getDataValid(): Boolean {
   return !data.expired
}
    • 所以只读变量也未必每次取得的值都是一样的
  • Kotlin 的get、set有专写法
    • 首先他们的声明要这样写
var name: String? = null
   get(){
     log("Name got")
     return field
   }
   set(value){
     field = value
     log("New name set")
   }
    • 注意field
  • 基本类型
    • 不再使用小写的int、float这些基本类型
    • 不再使用首字母大写的Integer、Float这些类
    • 而是统一换成了
      • Int
      • Float
    • 这几个类的java没有的,是kotlin自己造的
      • val count = 1 // Int
      • val pi = 3.14f // Float
      • 在kotlin里面写个1,他的类型的大Int
      • 也就是int、float在Kotlin里面被抛弃了
    • Kotlin里面不再有基本类型
      • 从kotlin代码编译出的字节码,当然会有JVM的基本类型,但在语言层面,kotlin是把java的基本类型完全阉割了
    • 这样很自然给我们一种不安全感
    • 会有什么我们暂时想不到的问题?有,但是都很好解决,等遇到了再说,完全没问题

补充

  • 可以表示继承,也可以表示实现
  • java的默认 public
  • 接口的定义和java一样
  • 构造方法
    • Kotlin单独用关键字 constructor 与其他fun 区分
  • override是由注解形式变成了关键字
    • 省略了了protected
    • 也就是override函数可见性继承父类
      • 关闭override可见性的遗传性,只需在方法前加一个final
      • final override fun onCreate(savedInstanceState: Bundle?) {}
  • java的类默认是 final 的
    • final类不可继承,可以用 open 解除
    • open没有父类到子类的遗传性
  • 强转和类型判断
    • is
fun main() {
    var activity: Activity = NewActivity()
    if (activity is NewActivity) {
        // 👇的强转由于类型推断被省略了
        activity.action()
    }
}
    • as
fun main() {
    var activity: Activity = NewActivity()
    (activity as NewActivity).action()
}
      • 上面这种如果强转失败会抛出异常
fun main() {
    var activity: Activity = NewActivity()
    // 👇'(activity as? NewActivity)' 之后是一个可空类型的对象,所以,需要使用 '?.' 来调用
    (activity as? NewActivity)?.action()
}

2.Kotlin里需要注意的地方

构造函数

  • 定义
    • 构造函数统一叫constructor
    • init代码块需要加init前缀
      • java {}
      • kotlininit{}

final

  • 定义
    • java 中用他修饰变量表示变量值只能被赋值一次
    • Kotlin中直接把var 换成val
      • 虽然val修饰的变量不能被二次赋值,但是也可以通过自定义变量的get方法使值是动态的
  • 常量
    • java的常量里面我们是最喜欢用final的
    • static+final
      • kotlin里面static的写法也不一样
      • kotlin里静态变量和静态方法这两个概念被去掉了
      • kotlin里面静态变量和方法的等价写法
        • 使用一个叫companion object 的东西
class A {
    companion object {
        var SITE = "haha'"
    }
}

object

  • kotlin里object是个什么东西?首字母小写的
  • object
    • Kotlin 的 object 不是一个类,而是一个关键字
    • 他最基本的用法是去修饰一个类,替换class关键字
      • 他的意思是:创建一个类,并且创建这个类的对象,这个就是object本身的意思,对象
      • 在程序的其他地方使用这个对象,可以用类名直接访问,这是什么?这不就是单例
object A {
    val site = "haha" 
    fun printSite() = println(site)
}
val siteName = A.site 
A.printSite()
      • 这个单例的内部字段和方法的使用方式,是不是特别像静态字段和静态方法
      • 所以object也是Java 的static 的替代品之一
      • 当你给一个类用了object方法,这个类的所有方法都相当于静态方法,因为他本质上是个单例对象。没得选,全都是类名访问的。
    • objet也可以用来创建匿名类对象

    • 当你希望某个类不是单例,但同时又要有静态函数和变量,这个时候用object 就不行了

      • 但是你可以在这个类的内部写一个object
object A {
    val name = "haha"
    object B {
        val site = "haha" 
        fun printSite() = println(site)
    }
}
val siteName = A.B.site
      • 这个时候可以将B匿名,用companion代替,这样就可以直接用A的类名调用,这样就跟java里面的静态变量和静态方法等价了
class A {
    val name = "haha"
    companion object {
        val site = "haha" 
        fun printSite() = println(site)
    }
}
val siteName = A.site
    • 不过上面这种不算kotlin推荐的写法,Kotlin里面有一种顶层声明的写法

顶层声明

  • 定义
    • 叫 top-level declaration
    • 就是把属性和函数声明不写在class里面
val site = "haha"
fun printSite() = println(site)
class A {}
    • 这样写的属性和函数不属于任何class,而是直接属于package,他和静态变量、静态方法一样是全局的,但他用起来更方便,使用的时候就连类名都不用写
      • companion object 对比
        • 顶层声明是全局的,一般是写工具类,这样直接用方法名试就可以知道这个类有没有被用过
        • companion object 一般是某个类单独用的属性,比如activity 打印的TAG 名

常量

  • 定义
    • Java里面写常量用的是 static final
    • Kotlin里面有一个关键字专门用来写常量const
    • 不过它针对的是编译期常量,compile-time constant
      • 编译期常量是在java就有的概念
      • 它是编译期在编译的时候就知道,这个变量在实际运行时的每个调用的实际值
      • 因此,这个变量可以在编译时直接把这个值硬编码到所有调用处
      • 具体来说,就是这个东西不仅要是 static final,而是只能是基本类型或String 。因为这些类型在不重新赋值的情况下,是不能提供调用内部方法来修饰内容的
class A {
    companion object {
        const val TAG = "haha" 
    }
}

数组

  • 定义
    • java的数组到了Kotlin里面变成了和集合类一样的泛型式写法
val strs: Array<String> = arrayOf("a","b","c")
...
println(strs[0])
strs[1] = "B"
    • 具体的使用跟java的数组是一样的,另外还可以用get和set函数
println(strs.get(0))
strs.set(1, "B")
    • Kotlin 的Array在编译成java的子界面的时候,用的依然是Java的数组
    • 但是由于语言层次改成了泛型实现,因此Kotlin的数组失去了协变(covariance)的这个特性
      • 也就是说,在Kotlin里面,不能把子类的数组对象,赋值给父类,这个在java里面是没问题的。
      • Kotlin这样做不是为了收窄功能,而是为了给数组添加一系列的工具方法,这样数组的实用性就大大增加了
Array.forEach()
Array.reverse()
Array.filter()
Array.sort()
Array.any()
Array.count()
Array.find()
Array.map()
Array.flatMap()
Array.find()
Array.fold()
Array.groupBy()

集合

  • 定义
    • Kotlin对集合类也进行了重写,创造了自己的一套List、Set、Map类型,目的也是给他们扩功能
    • 使用
      • val strList: List<String> = listOf("a","b","c")
      • val strSet: Set<String> = setOf("a","b","c")
      • val strMap: Map<String, String> = mapOf("a" to "A","b" to 'B")
    • Kotlin的list是不可变的,也就是不能添加、修改、删除元素
      • 如果要修改,需要用可变的list
        • val strList: MutableList<String> = ListOf("a","b","c")
      • 不可变List除了不可变这个限制,也多了一个特性,他是协变的,你可以把子类的list赋值给父类的list
      • 另外,Kotlin 是有类型推断的,很多时候数组和集合的类型也可以不用标明
        • val strList = ListOf("a","b","c")
        • val strSet = setOf("a","b","c")
        • val strMap = mapOf("a" to "A","b" to 'B")
        • val strs = arrayOf("a","b","c")
      • 所以Kotlin里的数组和可变List的API是非常像的
        • 最本质的区别是Kotlin的数组内部本质上还是java的数组
        • 所以继承了java数组的元素个数不可变的性质
        • 跟list比起来会有一点不方便
        • 那我要数组干嘛?
          • 这个问题在java中就存在了,Java的数组和List功能这么相似,我到底用谁?
          • 都可以,只不过List用起来更舒服一些,功能也更多一些
          • 由于java基本类型的数组没有自动装箱和拆箱,而List是有的,所以对于基本数据类型,数组的性能会比List好一些
          • 不过在Kotlin里用基本数据类型的数组,要用专门的数组类才能免于自动装箱和拆箱
            • val ints: IntArray = intArrayOf(1,2,3)

3.Kotlin中比较好用的变化

主构造函数

  • constructor用法
class User{
  var name: String
  constructor(name: String){
    this.name = name
  }
}
    • kotlin中还有更方便的写法,主构造函数
class User constructor(name: String){
  var name: String
  //{
   // this.name = name
  //}
}
    • 而且可以给属性赋初始值的时候,可以直接使用主构造函数里的参数值的
class User constructor(name: String){
  var name: String = name
}
    • 而且可以直接在这里声明属性,这样就连赋值的代码都省了
      • class User constructor(var name: String){}
  • 用在哪?
    • java的构造方法也是一种方法,只不过是用于创建对象的特殊方法
    • Kotlin里面同样,构造函数也是一种用于创建对象的特殊的函数,方法和函数,名字不同,作用完全一样
      • 函数最重要的是他的函数体,也就是大括号里面的代码,这里面描述了函数的行为
      • 但Kotlin的主构造函数没有函数体
      • Kotlin的主构造函数的定位并不是一个函数,而是一个入口,一个接受参数的入口,接受参数用来初始化
      • 初始化对象主要有两个地方
        • 一个是属性的初始赋值
class User constructor(name: String){
  var name = name
}
        • 另一个是init代码块,在init代码块里面也可以直接使用主构造函数里接受到的参数
class User constructor(name: String){
  var name = name

  init {
    println("Name got: $name")
  }
}
      • 次构造函数:主构造函数是可选的,如果使用主构造函数,就一定要在每一个次构造函数里调用这个主构造函数
class User constructor(name: String){
  var name = name

  init {
    println("Name got: $name")
  }

  constructor() : this("haha"){}
}
        • 不然初始化过程是缺失参数的
      • 便捷写法:如果你在主构造函数的参数左边写上var或者val,那么这个参数除了被当做参数之外,Kotlin还会帮你在类里自动创建一个和参数同名的属性,并且把这个参数的值作为属性的初始值
        • class User constructor(var name: String){}
        • 上面代码等价于
class User constructor(name: String){
  var name: String = name
}
    • 主构造函数虽然没有函数体,属性的初始化代码和init代码块就相当于他的函数体
class User constructor(var name: String){
  init {
    println("Name got: $name")
  }
}
      • 相当于
class User{
  var name: name

  constructor(name: String) {
    this.name = name
    println("Name got: $name")
  }
}
    • 如果有多个主构造函数,哪个写成主的呢?
      • 把最基本那个写成主的,其他的调用他

Kotlin的普通函数也有一些简便的写法

  • 如果函数只有一行代码
fun sayHi(nema: String){
  println("Hi " + name)
}
    • 可以把大括号去掉,把函数体加个等号直接写在右边
      • fun sayHi(name: String) = println("Hi " + name)
    • 也可以给参数配置默认值
      • fun sayHi(name: String = ”world" ) = println("Hi " + name)
      • 就可以在调用函数的时候选择填参数或者不填
        • sayHi("haha") //打印 Hi haha
        • sayHi() //打印Hi world
    • 有了函数默认值的支持,Java里的同名重载函数,在Kotlin中大部分都可以写在同一个函数里了
  • 可以在函数里面定义函数
fun printUsers(users: List<User>){
  fun printUser(user: User){
    println("User: " + user.name)
  }
  for(User in users){
    printUser(user)
  }
}
    • 就像局部变量一样,这种函数就叫局部函数local function
  • Kotlin字符串也提供了和大多数现代语言一样的字符串模板的支持
    • println("Hi: $name")
    • 你可以在字符串中插入变量
    • 这种变化对我们Android程序员来说并不陌生
      • 首先,gradle所用的语言Groovy本来就有这种支持
      • 而且我们在Android的资源文件里定义字符串,也一直在这么用
        • <string name="hi">Hi %s</string>
  • Kotlin自己定义了一套数组和集合的类型,目的就是为了提供一套方便的操作方法,用这一套东西可以对数组和集合做各种便捷的整体化操作
  • Kotlin还额外提供了RangeSequence这两种新的类型
    • Range是个自动化的数数工具
      • val range = 1..6
    • 而Sequence可以在数据量比较大或者数据量未知的时候提供方便的流式处理方案
      • val sequence = sequenceOf(-1,0,1,2,3,4,5,6)
  • 条件控制优化
    • switch变成了when
when(name.length){
  0 -> println("Empty!")
  1,2 -> println("Too short!")
  else -> println("OK.")
}
  • 判断相等的时候
    • a == b
      • 内容比较:对基本数据类型及String比较,相当于equals
    • a === b
      • 对引用的内存地址进行比较,相当于java中的==

4.Kotlin的泛型

in和out关键字

  • 说到Kotlin的泛型,很多人会想到 in 和 out 两个关键字,这是java里面没有关键字
  • 在java里他们就是带上界下界通配符?号
    • var textViews: List<out TextView> = ...
    • var textViews: List<in TextView> = ...
  • 等价于
    • List<? extends TextView> textViews = ...
    • List<? super TextView> textViews = ...

java里用泛型是什么场景?

  • 集合类
    • 这种写法在Kotlin里面也是可以用的
      • List<TextView> textView = new ArrayList<TextView>();
      • var textViews: MutableList<TextView> = ArrayList<TextView>()
    • 由于编译期会做类型推断,可以简化成这样
      • var textViews: MutableList<TextView> = ArrayList()
    • 有一种看起来没问题的代码,是不被允许
      • List<TextView> textViews = new ArrayList<Button>();
      • var textViews: MutableList<TextView> ArrayList<Button>()
      • 不能把一个子类的List对象赋值给父类的List引用
      • 这是java泛型不支持的一种性质:协变
        • 协变来着数学上的一种概念
        • 他在泛型中的意思是:子类的泛型类型,也属于泛型类型的子类。
          • 就是你声明一个父类的List,我给你赋值过来一个子类的List对象也是可以的。
        • 但是Java的泛型不具备这种性质
      • java的这种限制是因为Java泛型在编译时的类型擦除
        • 由于有类型擦除的存在,为了保证类型的安全,java给泛型设置了这种限制
      • 在java的数组做类似的事情是不会报错的
        • TextView[] textViews = new Button[10];
        • 这是因为数组没有在编译时擦除类型
      • 什么是类型擦除?什么是类型安全?类型擦除为什么会导致类型不安全?
        • 他的理解能过对你整体搞懂泛型很有帮助
        • 查资料去吧
      • 解除限制:java通过了相应的语法来解除限制,就是那个通配符?号
        • List<?extends TextView> textViews = new ArrayList<Button>();
        • var textViews: MutableList<TextView> ArrayList<Button>()
        • 在声明处的类型参数左边写上?extends,就可以把子类的泛型对象赋值给父类泛型类型声明了
        • 这种写法虽然解除了赋值的限制,却会另外增加一个限制
          • 在使用这个应用的时候,你不能调用他的参数包含类型参数的方法
            • 如:
              • List<?extends TextView> textViews = new ArrayList<Button>();
              • textViews.add(tv);//这里报错
            • 类型参数就是那个E
              • public interface List<E> extends Collection<E>{
              • ...
              • boolean add(E e);
              • }
          • 也不能给他的包含类型参数的字段赋值,除了空值
        • 简单来说,对应的out就是你只能使用他,不能修改他
        • in相反,表示你只能写我不能读我

他有什么用?

  1. 扩大选择类型范围
    • 比如你有一个方法,他的功能是接收一个TextView的List,遍历打印出他们的文字内容
public void printTexts(List<TextView> textViews){
  for(TextView textView: textView){
    System.out.println(textView.getText());
  }
}
    • 按照java的规矩,如果我传入的是一个类型参数为Button的List对象,编译器是不允许的(补充一下,Button 是TextView 的子类)
      • List<Button> buttons = ...;
      • printTexts(buttons);//报错
  1. 还可以直接用在泛型类型声明时的类型参数上
interface Producer<out T>{
  fun produce(): T
}

interface Consumer<in T>{
  fun consume(product: T)
}
    • 他表示我的这个类型只用来输入或者输出
      • 他的作者根据他的功能判断出,他所有的使用场景都是只用来输入或者输出
      • 为了避免我在每个使用的位置都给变量写上out这么麻烦,那么我就直接在类型创建的时候写上

kotlin还可以在变量声明时用*号来填写类型参数

  • 相当于不带extends也不带super的?号
  • 术语上叫Unbounded Wildcard,就是没有上界也没有下界的意思
  • java里的纯问号相当于?extends Object,Kotlin的*意思也差不多,相当于out Any
  • 另外,如果你的类型声明里,已经有了out或者in,那这个限制在变量声明时也依然存在,不会被*覆盖
    • 比如你的类型声明里是out Number
interface Counter<out T : Number>{
  fun count(): T
}
    • 那他加上*之后效果就不是out Any,而是out Number
      • var counter: Counter<*> = ...
      • 其实还是:var counter: Counter<out Number> = ...

java里的泛型声明的时候可以通过extends设置上界

  • class Monster<T extends Animal>{...}
    • 注意这个是类型声明时候的上界
    • 和刚才说的那个声明变量和方法参数时候的那个带问号的上界不是一个东西
  • 这个上界是可以多重的,用&符号连接
    • class Monster<T extends Animal & Food>{...}
    • 而在Kotlin里面这个上界的设置从extends变成了:号
      • class Monster<T : Animal>{}
      • 这个很好理解,Kotlin的各种extends都被换成 冒号
      • 不过多重上界的设置,就从语法上和java不一样了,不能直接用&符号去连接多个上界
      • 而是在超过一个上界的时候,要把他们从尖括号里拿出来写,并且加上where前缀
        • class Monster<T> where T : Aniaml, T : Food{...}
      • 有人看文章的时候觉得这个where是个新东西,其实虽然java里没有where,但他并没有带来任何新功能,而是把一个老功能换了一个新写法

reified关键字

  • 这个关键字是java里面没有的
  • java里的泛型类型参数T并不是一个真正的类型,而是一个代号,所以不能把他当做一个真正的类型来用
    • 比如不能在方法里检查一个对象是不是一个T 的实例
<T> void printIfTypeMatch(Object item){
  if(item instanceof T){}//instanceof这里报错
}
    • 这个在kotlin和java里面都是一样的
    • 而在Kotlin里面只要加一个reified就可以解除这个限制
inline fun <reified T> printIfTypeMatch(item: Any){
  if(item is T){}
}
      • 不过kotlin自身有个限制,只能用在inline函数上,具体原因后面详细讲

5.协程

协程是什么

  • 协程本身是一个跟线程类似的,用于处理多任务的概念
  • 但在Kotlin里,协程就是一套由Kotlin官方提供的线程API
    • 就像Java的Executor和Android的AsyncTask
val executor = Executors.newCachedThreadPool()
executor.execute{
  ...
}
  • Kotlin的协程也对Thread相关的API做了一套封装
  • 让我们不用过多关心线程也可以方便的写出并发操作
    • launch{...}

协程好在哪?

  • 既然java有了Executor,Android又添加了HandleAsyncTask,而且现在我们又有了Rxjava这种神奇的瑞士军刀,那我要协程干嘛?
  • 协程的好处本质上跟那些其他的线程API一样,方便,但由于他借助了Kotlin语言上的优势,所以他比起那些基于Java的方案会更方便一点

非阻塞式挂起:

  • 最重要的是,他可以看起来同步的方式写出异步代码,这就是Kotlin最有名的非阻塞式挂起
    • val user = api.getUser()//网络请求(后台线程)
    • nameTv.text = user.name//更新UI(主线程)
    • 这是他最有用的特性,但很多人对它有误解,而且不少已经在用协程的人,对这个“非阻塞式”是有误解的,这些误解一旦得到传播,大家学习协程将会更加困难

协程长啥样:

  • 先来大概扫一眼协程长什么样,然后更好地了解协程到底好在哪
  • 协程最基本的功能是并发,也就是多线程
    • 用协程,可以把任务切到后台执行
launch(Dispatchers.IO){
  saveToDatabase(data)
}
    • 想切到前台也行
launch(Dispatchers.Main){
  updataViews(data)
}
    • 这种写法很简单,但他并不是协程相对于直接使用Thread的优势,因为Kotlin已经直接添加了一个函数来简化对Thread的使用
      • thread{...}
  • Kotlin协程最大的好处,是在于你可以把运行在不同线程的代码写在同一个代码块
launch(Dispatchers.Main){  //开始:主线程
  val user = api.getUser()  //网络请求:后台线程
  nameTv.text = user.name  //更新UI:主线程
}
    • 上下两行代码,线程切走再切回来,这是写java的时候绝对做不到的
    • 不过这个差别并不大,我们写回调早就习惯了,就算再用连续的网络请求,来让协程和被大家喷得体无完肤的回调地狱比较,对于习惯的人来说倒也还好,毕竟所谓的回调地狱出现的几率并不高,而且一般也就那么两三层
api.getUser()
     .enqueue(object : Callback<User>{
       ...
       override fun onResponse(call: Call<User>, response: Response<User>{
           runOnUiThread{
             nameTv.text = response.body()?.name
             }
           }
        })
    • 回调不止是多了几个缩进,他也限制了我们的能力
      • 比如我有一个需求,他需要分别执行两次网络请求,然后把结果数据合并后再显示到界面
      • 按照正常思路,这两个接口没有相关性,我应该同时发起请求,在都获取到结果之后,再在本地把两个结果融合
      • 但是回调式的开发要做这种工作很困难,于是我们可能会选择妥协,不并行请求了,先后请求吧,这就属于标准的”垃圾代码“,明明是两个可以并行的请求,而我由于自身能力不足而做成了串行的,导致网络请求时间延长一倍,也就是性能差了一倍,而用协程可以写出上下两行并行请求,然后在第三行把他们的结果合并
launch(Dispatchers.Main){
  val avatar = async{  api.getAvatar(user)}//获取用户头像
  val logo = async{  api.getCompanyLogo(user)}//获取公司的logo
  val merged = suspendingMerger(avatar, logo)//合并
  show(merged)//显示
}
      • 如果网络请求的关系更复杂,用协程写出来依然是清晰的上下行代码结构
    • 由于消除了并发任务之间协作的难度,协程让我们可以轻松写出复杂的并发代码。而且并发代码不难写,一些本来不可能实现的并发任务变成可能,甚至变得很简单,这些才是协程的优势所在

协程怎么用?

  • 最简单的使用
launch(Dispatchers.IO){
  val image = getImage(imageId)
}
    • 一个launch函数里面写上代码就可以切线程
    • 这个launch函数的含义是:我要创建一个新的协程,并在指定的线程上运行它
      • 这个被创建被运行的所谓协程就是你传给launch的代码,这段连续代码叫做一个协程
  • 什么时候用协程
    • 当你需要切线程或者指定线程的时候
    • 你要在后台执行任务?切
launch(Dispatchers.IO){
  val image = getImage(imageId)
}
  • 然后需要在前台更新界面?再切
launch(Dispatchers.IO){
  val image = getImage(imageId)
  launch(Dispatchers.Main){
    avatarIv.setImageBitmap(image)
  }
}
    • 好像有点不对劲?这不还是回调地狱?
  • 如果只用launch函数,协程并不能做出比直接用线程更多的事,但是协程里面有一个很厉害的函数withContext()
    • withContext函数可以指定线程来执行代码
    • 并且在执行完成之后自动把线程切回来继续执行
launch(Dispatchers.Main){
  val image = withContext(Dispatchers.IO){
    getImage(imageId)
  }
  avatarIv.setImageBitmap(image)
}
    • 这种写法跟刚才的区别不大,但是你有了更多的线程切换,区别就提现出来了。由于有了自动切回来的功能,协程消除了并发代码在协作时的嵌套
launch(Dispatchers.IO){
  ...
    launch(Dispatchers.Main){
      ...
      launch(Dispatchers.IO){
        ...
      }
    }
}
      • 直接写成上下关系的代码,就能让多线程之间进行协作,这就是协程。协作式的例程
launch(Dispatchers.Main){
  withContext(Dispatchers.IO){
    ...
  }
  ...
  withContext(Dispatchers.IO){
    ...
  }
  ...
}
      • 而且由于消除了嵌套,你可以把withContext()放进函数的里面,用他包着函数的实际业务代码
launch(Dispatchers.Main){
  val image = suspendingGetImage(imageId)
  avatarIv.setImageBitmap(image)
}

fun suspendingGetImage(imageId: String){
  withContext(Dispatchers.IO){
    getImage(imageId)
  }
}
        • 不过直接这样试着写会报错:withContext()是一个suspend函数,他需要在协程里被调用,或者在另一个suspend函数里被调用

什么是suspend函数?

  • 点进withContext函数会发现他有一个suspend修饰符,有这种修饰符的就叫suspend函数,挂起函数
  • 为了不报错,我也要给我的函数加上这个修饰符
launch(Dispatchers.Main){
  val image = suspendingGetImage(imageId)
  avatarIv.setImageBitmap(image)
}

suspend fun suspendingGetImage(imageId: String){
  withContext(Dispatchers.IO){
    getImage(imageId)
  }
}
  • suspend到底是什么?
    • suspend是Kotlin协程里面最核心的关键字
    • 国内外所有介绍Kotlin协程的文章演讲都要提到他,但他也是最难懂和最多被误解的点
    • 代码执行到suspend函数的时候会suspend挂起,并且这个挂起非阻塞式的,他不会阻塞你的线程

6.协程的挂起

  • 首先挂起的是什么?
    • 线程?函数?都不是,挂起的是协程
  • 协程就是launch里面的代码,除了launch还有一个创建的协程的函数async()
launch(Dispatchers.Main){
  ...
  val image = suspendingGetImage(imageId)
  avatarIv.setImageBitmap(image)
}
    • launch创建这个协程,在执行到某个suspend函数的时候,这个协程会被挂起,从当前线程挂起
    • 说白了就是这个协程从正在执行他的线程上脱离
    • 注意不是这个协程停下来了,虽然suspend有暂停的意思,而是这个协程所在的线程,从这行代码开始不再运行这个协程了
    • 那么从现在开始我们就要兵分两路去分别看一看这两个分离了的线程协程各自发生了什么
      • 首先线程和协程分离了,具体到代码是什么意思?
        • 协程的代码块在线程里到了suspend函数的时候,突然执行完毕了,返回了。
        • 完毕之后线程干嘛呢?该干嘛干嘛去。
          • 如果这个线程是一个后台线程,那么接下来他可能就没事干了,或者去执行别的后台任务。总之和Java的线程池里的线程在做完事之后是一样的,要么回收掉,要么再利用
          • 如果这个线程是Android的主线程,那么他接下来就会回去继续工作,什么叫回去继续工作
            • 首先,如果你启动一个执行在主线程的协程,他实际上会往你的主线程post()一个新任务,这个任务就是你的协程代码,那么当这个协程被挂起的时候,实质上就是你的post()的这个任务提前结束了
            • 那这个时候主线程该干嘛?继续刷新界面。那剩下的代码怎么办?协程不是还没执行完吗?刚才我说什么?兵分两路,看完线程,我们就来看一下线程和协程分离之后,协程发生了什么
      • 线程和协程分离之后协程发生了什么?
        • 函数的代码到达挂起函数的时候被掐断了,所以接下来他会从这个挂起函数开始继续往下执行,不过是在指定的线程。谁指定的?挂起函数指定的
        • 比如这个例子里就是函数内部的那个withContext()所指定的IO线程
launch(Dispatchers.Main){
  val image = suspendingGetImage(imageId)
  avatarIv.setImageBitmap(image)
}

suspend fun suspendingGetImage(imageId: String){
  withContext(Dispatchers.IO){
    getImage(imageId)
  }
}
        • 另外在挂起函数执行完成之后,协程为我们做的最爽的事就来了,他会自动帮我们把线程再切回来
          • 比如如果我的协程原本是在主线程运行的,那么这个所谓的切回来就是在挂起函数执行完成之后,协程会帮我们再post()一个任务,让我剩下的代码继续回到主线程去执行
          • 这就是为什么你指定的线程的那个参数不叫Threads,而是叫Dispatchers,调度器,他不只能指定协程执行的协程,还能在suspend挂起函数之后自动再切回来
          • 其实也不是一定会切回来,你也可以通过设置特殊的Dispatcher,来让挂起函数执行完成之后也不切回来,不过这是你的选择,而不是他的定位
          • 挂起的定位就是”暂时切走,稍后再切回来

总结什么是挂起?

  • 协程在执行到有suspend标记的函数,也就是挂起函数的时候,他会被suspend挂起,而所谓的挂起其实跟开启一个协程一样,说起来比较邪乎,但其实就是切个线程,只不过挂起函数在执行完毕之后,协程会自动的重新切回他原来的那个线程,所以所谓的挂起就是一个稍后会被自动切回来的线程的切换
  • 那个切回来的动作,在协程里面叫做resume恢复
  • 为什么挂起函数只能在协程里?或者另一个挂起函数里面被调用
    • 首先挂起之后是需要恢复的,也就是把线程给切回来,而恢复这个功能是协程的,所以如果你一个挂起函数不在协程里面被调用,那么这个恢复的功能就没法实现
    • 另外,你想一下这个逻辑,如果一个挂起函数他要么在协程里面被调用,要么在另外一个挂起函数里面被调用,那么实质上直接或间接地要在一个协程里面被调用

挂起是怎么做到的?

  • 首先可以试着自定义一个挂起函数,然后在主线程上的协程里去调用他,你会发现他还是在主线程,没有切换
suspend fun suspendingPrint(){
  println("Thread: ${Thread.currentThread().name}")
}

launch(Dispatchers.Main{
  suspendingPrint()
}
  • 为什么没有切?因为他不知道往哪里切
  • 之前自定义的挂起函数,他里面是有一个withContext(),而且这个withContext()他也是一个挂起函数,他接收了一个Dispatcher参数,你的withContext()才知道往哪切,接着你的协程才被挂起,也就是切到别的线程去了
suspend fun suspendingGetImage(imageId: String){
  withContext(Dispatchers.IO){
    getImage(imageId)
  }
}
  • 也就是说,所谓的协程被挂起,或者说切线程这件事,他并不是发生在你外部这个挂起函数被调用的时候,而是里面那个挂起函数withContext()被调用的时候
  • 当然,如果你再往代码里面去钻一下,你会发现这个withContext()也不是真正切线程的点,而是他内部某一行代码
  • 所以suspend关键字其实并没有起到任何的把线程挂起或者说切换线程的作用
    • 真正挂起线程还需要你在挂起函数里面去调用另外一个挂起函数。
    • 而且里面这个挂起函数,他需要是协程自带的、内部实现了协程挂起代码的,或者他不是自带的,但他的内部直接或者间接地调用了某个自带的挂起函数。
    • 总之你最终需要调用到一个自带的挂起函数,让他来做真正的挂起,也就是切换线程的工作
    • 自带的挂起函数不只withContext()一个,他们都能实现协程的挂起。
    • 而我们要想自己写一个自定义的挂起函数,就需要在这个挂起函数的内部直接或者间接地调用某个自带的挂起函数,只加关键字suspend是不行的,这个关键没有那么神奇
  • 既然这个suspend关键字不能做到挂起,那他的作用是什么
    • 这是一个看起来学术,但其实非常实际的,对写代码非常有帮助的问题
    • 有的人已经研读过Kotlin协程的代码,知道了编译器是怎么实现了协程,也知道了Kotlin是怎么实现了挂起的,但今天我们抛开这些底层实现的问题,我们就看语法。
    • 在语法上,这个suspend关键字,他到底是什么作用?具体点说,Kotlin为什么会给我提供这个关键字让我用呢?他对于协程的挂起并没有任何实质上的作用,那他到底是干嘛的呢?或者说为什么这个关键字不干脆就直接删掉?
    • suspend关键其实是一个提醒,函数的创建者对函数的调用者的提醒,我是一个耗时函数,因此我被我的创建者用挂起的方式放在了后台运行,所以请在协程里调用我。提醒调用者,我耗时,这个提醒让我们的主线程不卡
    • 我们在写Java的时候在主线程做事需要非常小心,一旦你不留神在主线程调用了一个耗时方法,就会在这卡一下,而且这种事是很难避免的,因为我有不知道哪个方法会耗时,对吧?又不是我写的,就算是我写的,万一我忘了呢
    • 而协程通过挂起函数这种形式,他耗时任务切线程这个工作,实际上交给了函数的创建者,而不是调用者
    • 对于调用者来说,事情非常简单,他只会收到一个提醒,你需要把我放在协程里面,而通过这种方式,suspend这个关键字实际上作为一个提醒,是形成了一种机制,一种让所有耗时任务全都自动放在后台执行的机制,那么主线程是不是就不卡了
    • 所以为什么suspend的关键字他并没有实际去操作挂起,但Kotlin却给我们提供出来让我们使用,因为他的定位就不是用来去操作挂起的,挂起的操作靠的是挂起函数里面的实际代码,而不是这个关键字
    • 实际上,如果你创建一个挂起函数,但不在他的内部调用别的挂起函数,Android Studio会给你一个提醒,告诉你suspend是多余的,为什么多余?
suspend fun suspendingPrint(){
  println("Hello")
}
      • 因为你这个函数实质上并没有发生挂起,那你这个suspend关键字就只有一个效果,限制这个函数在协程里被调用,这个肯定没必要是吧?你又不去挂起协程,为什么还要限制在协程里才能调用呢?
      • 所以说,你创建一个挂起函数,一定要在他内部调用别的挂起函数,你的这个挂起才能是有意义的
      • 那么在了解挂起函数到底是什么,suspend这个关键字到底有什么意义之后,我们就可以进入下一个话题,怎么自定义一个挂起函数?

怎么自定义一个挂起函数?

  • 什么时候需要自定义
    • 如果你的某个函数比较耗时,那就把他写成挂起函数,这就是原则
    • 一般就两类
      • IO操作
        • 文件的读写,网络的交互
      • 计算工作
        • 图片的模糊或者美化处理
    • 耗时还有一种特殊情况,这件事本身做起来并不慢,但他需要等待,比如5秒之后在做这个操作,这种也是挂起函数的应用场景
  • 怎么写?
    • 给函数加上suspend关键字
    • 然后用withContext()把函数的内容包住就可以了
  • 其他suspend函数
    • 不是说除了withContext还有别的挂起函数吗?为什么你说自定义挂起函数要用withContext()?那别的不能用吗?
    • 不是别的不能用,而是withContext()最常用,也是最适合用来上手的,因为他的功能最简单也最直接,把线程切走再切回来
    • 别的挂起函数功能总会比他多一些或者少一些
    • 比如有个挂起函数:delay(10)
      • 他的作用是等待一段时间之后再去往下执行代码
      • 你能用他来写自定义挂起函数吗
      • 当然可以,比如刚才那种等待类型的耗时操作就可以用delay来做
suspend fun suspendUntilDone(){
  while(!done){
    delay(10)
  }
}
      • 只不过你不用第一时间就去接触他以及其他的一些自带的挂起函数,你可以把协程用的熟悉一点再说,不着急

7.什么是非阻塞式

  • 非阻塞式本质上是不卡线程这件事
  • 协程卡线程吗?当然不卡了,线程都切走了
    • 比如你本来在主线程执行
    • 忽然一个挂起函数切到后台去处理图片,那你的主线程会卡吗?肯定不卡
    • 但是我们要知道,这是你用协程
    • 但是如果你在主线程手动用Java自带的Thread或者线程池去切线程,你的主线程会卡吗?依然也是不卡的
    • 那么用Java 的Thread切线程,这个是非阻塞式吗?其实他也是的,是非阻塞式
    • 非阻塞式不是协程挂起的独有特性吗?怎么线程也有了?
  • 协程挂起的独有特性吗?
    • 在网上有一种说法是:协程的挂起是非阻塞式的,而线程是阻塞式的。这种说法是有严重误导性的。搞得好像协程的异步比线程的异步更高级一样。
    • 但其实“线程的阻塞式”指的是单线程是阻塞式的,因此单线程中的耗时代码会卡线程。
    • 而协程,单协程也可以是非阻塞式的,因为他可以利用挂起函数来切线程,但实际上Kotlin协程的挂起就是切线程而已,他跟Java的切线程是完全一样的,只是在写法上,上下两行连续代码,协程可以悄悄地把线程切走再切回来,不会卡当前线程,这个就是所谓的“非阻塞式”挂起,而不用协程的话,上下两行连续代码只能是单线程的,那当然会卡线程了
    • 所以协程的挂起函数跟Java原始的线程切换,其实都是非阻塞式的,只是协程是一种“看起来阻塞,但实际上却非阻塞”的写法而已
  • 网上还有一种说法:
    • 协程的这个“非阻塞式”比线程更加高效?!,甚至有人给出了一个详细的解释
    • 如果用线程处理网络请求,那么在网络请求返回之前,线程会一直等着他,是处于阻塞状态不做事的,这就导致了线程的利用率不高
    • 而如果用协程,由于协程在等待网络请求的过程中会被挂起,线程没有被阻塞,这就提高了线程的利用率
      • 听着好有道理,但是这种说法是错误的
      • 首先,所有代码本质上都是阻塞式
      • 而只有比较耗时的代码才能导致人类可感知的等待,比如你的主线程做一个几十毫秒的操作,他就会导致你的界面卡掉几帧,这是我们人眼可以观察出的,而这就是我们通常意义所说的阻塞
      • 而耗时操作,一共分为两种,
        • CPU的计算耗时和IO的耗时,
          • 而网络就属于IO,他的性能瓶颈是IO,也就是和网络数据的交互,而不是CPU的计算速度,所以线程会被网络交互所阻塞,但这个阻塞是不可避免的,你必须做这个IO,
          • 那他比较慢怎么办?没办法。你只能让线程在这里慢慢处理。但是要注意,他是在“慢慢处理”,而不是单纯的等待,他等待只是因为网络传输的性能低于CPU的性能,但他本质上是在做工作的。
      • 这种阻塞不可避免,那协程不是就可以挂起吗?
        • 协程的挂起本质是切线程
        • 网络请求的挂起也是切线程。他是把主线程给空置出来,然后去后台线程去做网络的交互。
        • 而不是先切到后台去做网络请求,然后网络请求到达了所谓的“等待阶段”的时候再挂起一次,通过这种方式让后台的这个网络交互线程空出来,然后这个网络交互线程就能去立即做别的网络请求,就不要傻等了。没这种好事!
        • 你把网络线程空出来,他就能立即去做下一个网络请求了?那你刚才正在等待的网络请求怎么办?他还是会有另外一个线程来承载的呀,不然你觉得这个网络交互会凭空地自己完成?不可能的兄台,挂起的本质就是切线程,只是他在完成之后能够自动地切回来,没有别的神奇之处了
        • 所谓协程的非阻塞式挂起,只是用阻塞的方式写出了非阻塞的代码而已,并没有任何相比于线程更加高效的地方
  • 那协程和线程到底是什么关系?
    • 别的语言不说,在Kotlin里,协程就是基于线程实现的一套更上层的工具API。类似于Java自带的Executor系列API,以及Android的Handle系列API
    • 那他和线程有什么关系呢?就像Handle API一样,协程就是一套基于线程的上层框架

8.Lambda

  • Kotlin可以用Lambda,Java8也有Lambda,挺好用的
    • val sum = { x: Int, y: Int -> x + y}
  • 听说Kotlin的Lambda还能当函数参数?
items.fold(0, {
  acc: Int, i: Int -> 
  print("acc = $acc, i = &i, ")
  val result = acc + i
  println("result = $result")
  result
})
    • 挺好用的,我也来写一个
items.map(num  ->{
  num * 2
})
      • 报错了?

前言

  • Kotlin很方便,但有时候也让人头疼,而且越方便的地方越让人头疼,比如Lambda表达式
  • 很多人以为Lambda而被Kotlin吸引
  • 但很多人也因为Lambda被Kotlin吓跑
  • 其实大多数已经用了很久Kotlin的人,对Lambda也只会简单使用而已,甚至相当一部分人不靠开发工具的自动补全功能,根本就完全不会写Lambda。不过要讲Lambda,需要先从Kotlin的高阶函数Higher-Order Function 说起。

高阶函数

  1. 在Java里,如果你有一个a方法需要调用另外一个b方法,你在里面调用就可以
int a(){
  return b(1)+1;
}
    • 而如果你想在a调用的时候,去动态设置b方法的参数,你就得把参数传给a,再从a的内部把参数传给b,这都可以做到
int a(int param){
  return b(param)+1;
}
    • 不过,如果我想动态设置的不是方法的参数,而是方法本身呢?
      • 比如我在a的内部有一处对别的方法的调用,这个方法可能是b,可能是c,不一定是谁,我只知道我在这里有一个调用,他的参数类型是int,他的返回值类型是int,具体在a执行的时候,调用哪个方法,我希望可以动态设置,或者说我想把方法作为参数传到另外一个方法里面,这个可以实现吗?不行,也行
    • 在Java里是不允许把方法作为参数传递的,
      • 但是我们有一个历史悠久的变通方案:接口。
      • 我们可以通过接口的方式来把方法包装起来,然后把这个接口的类型作为外部方法的参数类型,在调用外部方法时,传递接口的对象来作为参数,如果到这里你觉得听晕了,我换个写法你再感受一下
public interface Wrapper{
  int method();
}

int a(Wrapper wrapper){
  return wrapper.method() + 1;
}
    • 我们在用户发生点击行为的时候会触发点击事件,
      • 所谓的点击事件,最核心的内容就是调用内部的一个OnClickListener的onClick()方法,而所谓的这个OnClickListener其实只是一个壳,他的核心全部在内部那个onClick()方法,
      • 换句话说,我们传过来一个OnClickListener,本质上是传过来一个可以稍后被调用的onClick(),只不过Java不允许传递方法,所以我们才把他包进了一个对象里来进行传递。
  1. 而在Kotlin里面,函数的参数也可以是函数类型的
    • 当一个函数含有函数类型的参数的时候,如果你调用他,你就必须传入一个函数类型的对象给他
fun a(funParam: Fun): String{
  return funParam(1)
}

fun b(param: Int): String{
  return param.toString()
}

...
a(b)
    • 不过在具体的写法上没有我的实例这么粗暴
      • 首先我写的Fun作为函数类型其实是错的,Kotlin没有这么一种类型来标记这个变量是个函数类型,因为函数类型不是一个类型,而是一类类型,因为函数类型可以有各种各样不同的参数和返回值的类型的搭配,这些搭配属于不同的函数类型
        • 例如:无参数无返回值
          • () -> Unit
        • 和单Int型参数返回String
          • Int -> String
        • 就好像Int和String是两个不同的类型,
          • 所以不能只用Fun这个词来表示“这个参数是个函数类型”,
          • 就好像不能用Class来表示“这个参数是某个类”。因为你需要指定具体是哪个函数类型,或者说这个函数类型的参数,他的参数类型是什么,返回值类型是什么,而不能笼统地说一句“他是函数类型”就完了,所以对于函数类型的参数,你要指明他有几个参数,参数的类型是什么以及返回值类型是什么
        • 那么写下来大概就是这个样子
fun a(funParam: (Int) -> String): String{
  return funParam(1)
}

fun b(param: Int): String{
  return param.toString()
}

fun c(param: Int): (Int) -> Unit{
  ...
}

...
a(b)
        • 看着有点可怕,但只有这样写,调用的人才知道应该传一个怎样的函数类型的参数给你
      • 同样的函数类型不只可以作为函数的参数类型,还可以作为函数的返回值类型,这种“参数或返回值为函数类型的函数”,在Kotlin里被称为高阶函数

高阶?

  • 这个所谓的高阶,总给人一种神秘感:阶是什么?哪里高了?
  • 其实没有那么复杂,
    • 高阶函数这个概念源自于数学中的高阶函数,在数学里,如果一个函数使用函数来作为他的参数或者结果,他就被称作是高阶函数,比如求导
    • 这种参数里有函数类型或者返回值是函数类型的函数,都叫高阶函数。这只是对着一类函数的称呼。Kotlin的高阶函数没有任何特殊功能
  • 另外,除了作为函数的参数和返回值的类型,你把她赋值给一个变量也是可以的,不过对于一个声明好的函数,不管是你要把他作为参数传递给函数还是要把他赋值给变量。都得在函数名的左边加上双冒号才行
fun a(funParam: (Int) -> String): String{
  return funParam(1)
}

fun b(param: Int): String{
  return param.toString()
}

fun c(param: Int): (Int) -> Unit{
  ...
}

...
a(::b)
val d = ::b
  • 这是为什么呢?
    • 如果你上网搜,你会发现这个双冒号的写法叫做函数引用 Function Reference,这个是Kotlin官方的说法。
    • 但是这又表示什么意思?表示它指向上面的函数?那既然都是一个东西,为什么不直接写函数名?而要加两个冒号呢?

函数类型对象

  • 因为加了两个冒号,这个函数才变成了一个对象
    • 在Kotlin里“函数可以作为参数”这件事的本质是函数可以作为对象存在,因为只有对象才可以被作为参数传递呀
    • 赋值也是一样的道理,只有对象才能被赋值给变量
    • 但Kotlin函数的本身的性质又决定了他没办法被当做一个对象,那怎么办呢?
      • Kotlin的选择是,那就创建一个和函数具有相同功能的对象
      • 怎么创建?
        • 使用双冒号
        • 在Kotlin里,一个函数名左边加上双冒号,他就不表示这个函数本身了,而表示一个对象,或者说一个指向对象的引用
        • 但这个对象可不是函数本身,而是一个和这个函数具有相同功能的对象
  • 怎么个相同法呢?
    • 你可以怎么用函数,就能怎么用这个加了双冒号的对象。
    • 但我再说一遍,这个双冒号的东西,他不是一个函数,而是一个对象。而是一个函数类型的对象
    • 对象是不能加括号来调用的对吧?但是函数类型的对象可以。因为这其实是个假的调用。他是Kotlin的语法糖。
    • 实际上你对一个函数类型的对象加括号、加参数,他正在调用的是这个对象的invoke()函数
fun a(funParam: (Int) -> String): String{
  return funParam(1)
}

fun b(param: Int): String{
  return param.toString()
}

fun c(param: Int): (Int) -> Unit{
  ...
}

...
a(::b)
val d = ::b
b(1)  //调用函数
d(1)  //对象d后面加上括号来实现b()的等价操作
(::b)(1) //对象::b后面加上括号来实现b()的等价操作
d(1) 等价于  d.invoke(1)
(::b)(1)  等价于  (::b).invoke(1)
  • 所以你可以对一个函数类型的对象调用invoke(),但不能对一个函数这么做
    • b.invoke(1) //错误
    • 因为只有函数类型的对象有这个自带的invoke可以用,而函数不是函数类型的对象
  • 函数不是对象,他也没有类型,函数就是函数,他和对象是两个维度的东西,
    • 包括双冒号加上函数名这个写法,他是一个指向对象的引用,但并不是指向函数本身,
    • 而是指向一个我们在代码里面看不见的对象,这个对象他复制了原函数的功能,但他并不是原函数
  • 这个是底层的逻辑,但是我知道这个有什么用呢?
    • 这个知识能帮你解开Kotlin高阶函数,以及接下来要讲的匿名函数Lambda的大部分迷惑
  • 比如我在代码里面有这么几行
fun b(param: Int): String{
  return param.toString()
}
...
val d = ::b

匿名函数

  • 那如果我想把d赋值给一个新的变量e
    • val e = d
    • 我等号右边的d是应该加双冒号还是不加呢?
      • 不用试也不用搜,想一想,这个是个赋值操作对吧?赋值操作的右边是个对象对吧?d是对象吗?是对象,b不是对象是因为他来自函数名,但d已经是对象了,所以直接写就行了
  • 要传一个函数类型的参数,
    • 或者把一个函数类型的对象赋值给变量,除了用双冒号来拿现成的函数使用,你还可以直接把这个函数挪过来写
a(fun b(param: Int):String{
  return param.toString()
})

val d = fun b(param: Int):  String{
  return param.toString()
}
  • 这种Kotin是不允许
  • 另外这种写法的话,函数的名字其实就没有用了,所以可以把她省掉
a(fun(param: Int):String{
  return param.toString()
})

val d = fun(param: Int):  String{
  return param.toString()
}
  • 这种写法叫做匿名函数,为什么叫匿名函数,因为他没有名字,等号左边的不是函数的名字,他是变量的名字,这个变量的类型是一种函数类型
  • 等号左边的不是函数的名字,他是变量的名字,这个变量的类型是一种函数类型,
    • 具体到我们的示例代码来说是一种只有一个参数,参数类型是Int,
    • 并且返回值类型为String的函数类型
  • 另外,其实刚才那种左边右边都有名字的写法,Kotlin是不允许的,右边的函数既然要名字也没有用,Kotlin干脆就不许他有名字了。
  • 所以你在Java里设计一回调的时候是这么设计的
public interface OnClickListener{
  void onClick(View v);
}

pubic void setOnClickListener(OnClickListener listener){
  this.listener  = listener;
}
  • 用的时候是这么用的
view.setOnClickListener(new OnClickListener(){
  @Override
  void onClick(View v){
    switchToNextPage();
  }
});
  • 在Kotlin里就可以改成这么写的了
fun setOnClickListener(onClick: (View) -> Unit){
  this.onClick = onClick
}
...
view.setOnClickListener(fun(v: View): Unit {
  switchToNextPage()
})

Lambda

  • 另外大多数情况下,匿名函数还能再简化一点,写成Lambda表达式的形式
fun setOnClickListener(onClick: (View) -> Unit){
  this.onClick = onClick
}
...
view.setOnClickListener( { v: View ->
  switchToNextPage()
})
  • 如果Lambda是函数的最后一个参数,你可以Lambda写在括号的外面
view.setOnClickListener() { v: View ->
  switchToNextPage()
}
  • 如果Lambda是函数的唯一参数,你还可以把括号去了
view.setOnClickListener { v: View ->
  switchToNextPage()
}
  • 另外这个Lambda是单参数的,他的这个参数也可以省略不写
view.setOnClickListener {
  switchToNextPage()
}
    • 单参数的时候只要这个参数不用就可以不写了,其实就算用也可以不写,因为Kotlin的Lambda对于省略的唯一参数有默认的名字:it
view.setOnClickListener {
  switchToNextPage()
  it.setVisibility(GONE)
}
  • 不过,这个Lambda这也不写那也不写的,他不迷茫吗?他是怎么知道自己的参数类型和返回值类型的
    • 我调用的函数在声明的地方有明确的参数信息吧
view.setOnClickListener() { v: View ->
  switchToNextPage()
}
    • 这里边把这个参数的参数类型和返回值类型写得清清楚楚吧
    • 所以Lambda才不用写的
    • 所以当你要把一个匿名函数赋值给变量,而不是作为函数参数传递的时候,如果也简写成Lambda的形式,就不能省略掉Lambda的参数类型了
val d = fun(param: Int): String{
  return param.toString()
}
  • 因为他无法从上下文中推断出这个参数的类型啊
val d ={
  return it.toString()
}
  • it部分报错
  • 如果你出于场景的需求或者个人偏好,就是想在这里省掉参数类型,那你需要给左边的变量指明类型
val d: (Int) -> String ={
  return it.toString()
}
  • 另外Lambda的返回值不是用return来返回,而是直接取最后一行代码的值
val d: (Int) -> String ={
 it.toString()
}
  • 这个一定要注意,Lambda的返回值别写return
    • 如果你写了,他会把这个作为他外层的函数的返回值,来直接结束外层函数
  • 另外因为Lambda是个代码块,他总能根据最后一行代码来推断出返回值类型,
    • 所以他的返回值类型确实可以不写
    • 实际上,Kotlin的Lambda也是写不了返回值类型的,语法上就不支持

匿名函数和Lambda到底是什么?

  • 匿名函数
    • 他可以作为参数传递,也可以赋值给变量
    • 但是刚才我们也说过了函数是不能作为参数传递,也不能赋值给变量的,那为什么匿名函数就这么特殊呢?
    • 因为Kotlin的匿名函数不是函数,他是一个对象。匿名函数虽然名字里有函数两个字,包括英文的原名也是Anonymous Function。但他其实不是函数,而是一个对象,一个函数类型的对象
    • 他和双冒号加函数名是一个东西,和函数不是,所以你才可以直接把她作为函数的参数来传递以及赋值给变量
a(fun(param: Int):String{
  return param.toString()
})

val d = fun(param: Int):  String{
  return param.toString()
}
  • Lambda
    • 同理,Lambda其实也是一个函数类型的对象而已,你能怎么使用双冒号加函数名,就能怎么使用匿名函数,以及怎么使用Lambda表达式
    • 这就是Kotlin的匿名函数以及Lambda表达式的本质:他们都是函数类型的对象
    • Kotin的Lambda和Java8的Lambda是不一样的
      • Java8的Lambda只是一种便捷写法,本质上并没有功能上的突破
      • 而Kotlin的Lambda是实实在在的对象
    • 在你知道了在Kotlin里,“函数并不能传递,传递的是对象”和“匿名函数和Lambda表达式其实都是对象”这些本质之后,你以后去写Kotlin的高阶函数会非常轻松
  • Kotlin官方文档里面对于双冒号加上函数名的写法叫做Function Reference(函数引用)。
    • 故意引导大家认为这个引用是指向原函数的,这是为了简化事情的逻辑,让大家更好上手Kotlin
    • 这是为了简化事情的逻辑,让大家更好上手Kotlin,但这种逻辑是有毒的,一旦你信了他, 你对于匿名函数和Lambda就怎么也搞不清楚了

怎么写?

  • 对于Kotlin的Lambda,很多从Java过来的人表示,好用是好用,但是不会写。你都不会写,那你是怎么会用的呢?
  • Java从8开始引入了对Lambda的支持,对于单抽象方法的接口,简称SAM接口(Single Abstract Method接口)。
    • 对于这一类接口,Java8允许你用Lambda表达式来创建匿名类对象,但他本质上还是在创建一个匿名类对象,只是一种简化写法而已。
    • 所以Java8的Lambda只靠代码的自动补全就基本能写了,
    • 而Kotlin的Lambda跟Java8本质上就是不同的,因为Kotlin的Lambda是实实在在的函数类型对象,功能更强,写法更灵活,所以很多人从java过来就有点搞不明白了
view.setOnClickListener(v -> {
  switchToNextPage();
});
  • 另外Kotlin是不支持用Lambda表达式来简写匿名类对象的,因为我们有函数类型的参数嘛。所以真正单函数接口的写法从根本上就没必要了。那你还支持他干嘛?
  • 不过当和Java交互的时候,Kotlin是支持这种用法的,当你的函数参数是Java的单抽象方法的接口的时候,你依然可以使用Lambda来写参数
view.setOnClickListener {
  switchToNextPage()
}
  • 但这其实也不是Kotlin增加了功能,而是对于来自Java的单抽象方法的接口
  • Kotlin会为他们额外创建一个,把参数替换为函数类型的桥接方法,让你可以间接地创建Java的匿名类对象
  • 这就是为什么你会发现,当你在Kotlin里调用View.java这个类的setOnClickListener()的时候,可以传Lambda来给他创建OnClickListener对象
  • 但你照着同样的写法写一个Kotlin的接口,却不能传Lambda
interface KotlinListener {
  fun onAction()
}

fun setKotlinListener(listener: KotlinListener){
  this.listener = listener
}

...
setKotlinListener {
  doSomething()//报错
}
  • 因为Kotlin期望我们直接使用函数类型的参数,而不是用接口这种折中方案
  • 不过Kotlin1.4之后开始支持了

总结:

  • 函数类型变量
    • 在Kotlin中有一类Java中不存在的类型:函数类型
    • 这一类类型的对象在可以当函数来使用的同时,还能作为函数的参数、函数的返回值以及赋值给变量
    • 创建一个函数类型的对象有三种方式
      • 双冒号加函数名
      • 匿名函数
      • Lambda
    • 一定要记住:双冒号加函数名、匿名函数、Lambda本质上都是函数类型的对象
    • 在Kotlin里,匿名函数不是函数

9.扩展函数和扩展属性

定义

  • 你可以给已有的去额外添加函数和属性
  • 而且既需要改源码也不需要写子类

使用

  • 基本使用:
    • 很多人用扩展都只用来写个叫dp的扩展属性,来把dp值转成像素值
    • 稍微高级一点就不太行 ,尤其是扩展函数和函数引用混在一起的时候就更是瞬间蒙圈
  • 幂运算
    • 在java里
      • 在java里我们如果想做幂运算,也就是几的几次方,要用静态方法pow(a,n),他是power的缩写,power就是乘方的意思
      • 这个pow方法是Math类的一个静态方法
        • 这个类方法我们用得比较多的是max()和min()
          • Math.pow(2,10);
          • Math.max(1,2);
          • Math.min(1,2);
        • 比较两个数的大小用静态方法很符合直觉,但是幂运算的话静态方法就不如成员方法来得更直观了
          • Math.pow(2,10);
          • 成员方法:2.pow(10);
        • 但我们只能选择静态方法,因为Integer、Float、Double这几个类没提供这个方法,所以我们只能用Math类的静态方法
    • 在Kotlin
      • 在kotlin里我们用的不是Java的Integer、Float、Double,而是另外几个名字相同或相近的Kotlin自己新创造的类,这几个类同样没有提供pow()这个函数
      • 但好的是,我们依然可以用看起来先像是成员函数的方式来做幂运算
        • 因为Float.pow(n:Int)是Kotlin个Float这个类增加的一个扩展函数
        • 在声明一个函数的时候在函数名的左边写个类名再加个点,你就能对这个类的对象调用这个函数了,这种函数就叫扩展函数(Extension Function)
          • public actual inline fun Float.pow(n: Int): Float = nativeMath.pow(this.toDouble(), n.toDouble()).toFloat()
          • 2f.pow(10)
          • 就好像你钻到这个类的源码里,改了这个类的代码,给他增加了一个新的函数一样
          • 虽然事实上不是,但是用起来一样
        • 这种用法对我们开发带来了很大的便利,举个例子
          • 比如pow()
          • 再比如,AndroidX里有个东西叫ViewModel
            • 很多人对ViewModel有很大误解,竟然以为这个是用来写MVVM 架构的
            • AndroidX的KTX库里有一个对于ComponentActivity类的扩展函数叫viewModels()
            • 只要引用了对应的KTX库,在Activity里就可以直接调用这个函数,来方便地初始化ViewModel,而不需要重写Activity类
class MainActivity : AppCompatActivity(){
  val model: MyViewModel by viewModels()
  ...
}
    • 类似的用法可以有很多很多,限制你的是你的想象力
      • 所以其实对于扩展函数,你更需要注意的是谨慎和克制,需要用了再用,而不要因为他很酷很方便就能用则用
      • 因为这些方便的东西如果太多,就会变成对你和同事的打扰
  • 扩展函数写在哪都可以,但写的位置不同,作用域也就不同
    • 最简单的写法就是把他写成Top Level,也就是顶层的,让他不属于任何类,这样你就能在任何类里面使用他,这也和成员函数的作用域很像
    • 那么这个函数属于谁?他属于包
    • 那为什么可以被这个类的对象调用呢?因为他在函数名的左边
fun String.method1(i: Int){
  ...
}
...
"haha".method1(1)
  • 在Kotlin,当你给声明的函数名的左边加上一个类名的时候,表示你要给这个函数限定一个Receiver,直译的话叫接受者,其实也就是哪个类的对象可以调用这个函数
    • 虽然说你是个Top-level Function,不属于任何类,确切地说不是任何一个类的成员函数,但我要限制只有某个类的对象才能调用你
    • 这就是扩展函数的本质
    • 这和成员函数有什么区别吗?
      • 除了写在顶层声明,扩展函数也可以写在某个类里面,然后你就可以在这个类里面调用这个函数
class Example{
  fun String.method1(i: Int){
    ...
  }
  ...
  "haha".method1(1)
}
  • 但必须使用那个前缀类的对象来调用他
    • 这个函数这么写他到底属于谁?
      • 再明确点,他是谁的成员函数?
        • 当然是外部类Example的成员函数了,因为他写在他里面
    • 那函数名左边的String是什么?
      • 他是这个函数的Receiver,也就是谁可以去调用他
    • 在之前的Lambda中,函数可以使用双冒号被指向
      • (Int::toFloat)(1) // 等价于 1.toFloat()
      • 其实指向的不是函数本身,而是一个和函数等价的对象,这也是为什么你可以对这个引用调用invoke(),却不能对函数本身调用
      • 为了简单起见,我们通常可以把这个指向函数等价的对象的引用称作是指向这个函数的引用
        • 基于这个叫法继续说
        • 普通函数可以被指向,扩展函数也是同样可以被指向
        • 如果这个扩展函数不是顶层声明的,也就是说如果他是某个类的成员函数,他就不能被引用
          • 因为有歧义,他是属于谁
      • 当你拿着一个函数的引用去调用的时候,不管是一个普通的成员函数还是扩展函数,你都需要把接受者或者调用者作为第一个参数传进去
fun String.method1(i: Int){
  ...
}
val a: String.(Int) -> Unit = String::method1
"haha".a(1)

a("haha",1)//等价于“haha”.a(1)

a.invoke("haha",1)//等价于“haha”.a(1)

(String::method1)("haha",1)//"haha",method1(1)

(Int::toFloat)(1)//  等价于 1.toFloat()
    • 让我们把第一个参数写上函数调用者

扩展属性

  • 除了扩展函数,还有扩展属性
var Float.dp
  get() = TypedValue.applyDimension(
  TypedValue.COMPLEX_UNIT_DIP,
  this,
  Resources.getSystem().displayMetrics
  )
...
val RADIUS =200f.dp

内联:Kotin源码里的inline、noinline和crossinline

  • inline
    • Kotlin里有个特别好用的关键字叫inline
    • 他可以帮助你对那些做了标记的函数进行内联优化,所谓内联就是调用的函数在编译的时候,会变成代码内嵌的形式
inline fun hello() {
  println("Hello!")
}
...
fun main(){
  hello()
}
    • 所谓内联就是调用的函数在编译的时候,会变成代码内嵌的形式
//实际编译的代码
fun main(){
  println("Hello!")
}
    • 这样的好处很明显,调用栈变浅了
    • 不过事实上,这种对调用栈优化的效果非常小,小到了应该被忽略的程度。因为这种优化不仅没啥用,而且可能会因为代码的多处拷贝而导致编译生成的字节码膨胀,从而变成负优化
      • 所以这种东西我们要他干嘛呢?
  • 编译时常量

    • Java里有个概念,叫做编译时常量Compile-time Constant,直观的讲就是这个变量的值是固定不变的,并且编译器在编译的时候,就能确定这个变量的值
    • 具体到代码上
      • 就是这个变量需要是final的,
      • 类型只能是字符串或者基本类型,
      • 而且这个变量需要在声明的时候就赋值,
      • 等号右边还不能太复杂
    • 总之就是你要让编译器一眼瞟过去就能看到结果,
      • 这种编译时常量会被编译器以内联的形式进行编译,也就是直接把你的值拿过去替换掉调用处的变量名来编译,这样一来程序结构就变简单了,编译器和JVM也方便做各种优化,这就是编译时常量的作用
  • 在Kotlin里

变量内联const

  • 这种编译时常量有了一个专有的关键字const
    • 一个变量如果以const val 开头,他就会被编译器当做编译时常量,来进行内联式编译
const val AUTHOR = "xxx“
...
fun main(){
  authorView.text = AUTHOR
}
//实际编译的代码
fun main(){
  authorView.text = "xxx"
}
    • 当然你得符合编译时常量的特征,不然会报错不给编
      • const val AUTHOR = author.name
    • 让变量内联用的是const,而除了变量,Kotlin还增加了对函数进行内联的支持

函数内联inline

  • 你给一个函数加上inline关键字,这个函数就会被以内联的方式进行编译
  • 但,虽然同为内联,inline关键字的作用和目的与const是完全不同的
  • 编译时常量为什么那么多限制?
    • 因为只有符合这些限制,编译器和JVM才有能力去做优化,这些内联操作才有意义,稍微复杂一点,就优化不动了。
    • 什么叫稍微复杂我不知道,但是函数内联绝对算得上是相当复杂,绝对优化不动的,
    • 其实真要较真起来,函数内联也确实会产生一种被动的优化,就是我刚才所说的,去掉一个函数,调用栈少了一层,性能损耗肯定会少一些,但实际上调用栈本身所造成的性能损耗本来就非常小,这个优化跟没优化差不多。
    • 这个事实可能不太符合我们的直觉,但你这样想一下,在我们见过的各种性能优化规范里面,你有没有见过少写几个方法来减少调用栈这样的优化策略?没有吧?因为这种优化是没有意义的
    • 而同时函数内联不同于常量内联的地方在于,函数体通常比常量复杂多了,而函数内联会导致函数体被拷贝到每个调用处
    • 如果函数体比较大,而调用处比较多,就会导致编译出的字节码变大很多,我们都知道编译结果的压缩是应用优化的一大指标,而函数内联对于这项指标明显是不利的
    • 所以靠inline来做性能优化?不存在的?那么inline是干嘛的
  • inline干嘛用的?
    • 事实上inline关键字不止可以内联自己的内部代码,还可以内联自己内部的内部的代码
    • 什么叫内部的内部?
      • 就是自己的函数类型的参数
    • 例如我把hello()函数的定义改成这样
fun hello(){
  println("Hello!")
}

fun main(){
  hello()
}
fun hello(postAction: () -> Unit){
  println("Hello!")
  postAction()//做点后续工作
}

fun main(){
  hello(fun() {
    println("Bye!")
  })
}
    • 给他增加一个函数类型的参数,相应的,在调用出也需要填上这个参数,我可以填成匿名函数的形式
    • 也可以简单点写成Lambda表达式
fun main(){
  hello{
    println("Bye!")
  }
}
    • 因为Java并没有对函数类型的变量的原生支持
      • Kotlin需要想办法让这种自己新引入的概念在JVM中落地
      • 而她想的办法是什么呢?
      • 就是用一个JVM对象,来作为函数类型的变量的实际载体,让这个对象去执行实际代码
      • 也就是说,在我对代码做了刚才的那种修改之后,程序在每次调用hello()的时候,都会创建一个对象,来执行Lambda表达式里的代码
//实际编译的代码
fun main(){
  val post = object : Function0<Unit> {
    override fun invoke(){
      return println("Bye!")
    }
  }
  hello(post)
}
      • 虽然这个对象是用一下之后马上就被抛弃,但他确实被创建了,这有什么坏处?
      • 其实一般情况下,还真没有什么坏处,多创建个对象算什么呀?但是你想一下,如果这种函数被放在循环里执行,内存占用是不是一下就飚起来了?
fun main(){
  for(i in 1..100){
    hello{
      println("Bye!")
    }
  }
}
      • 而且关键是你作为函数的创建者,并不知道也没法规定别人,在什么地方调用这个函数,也就是说,这个函数是否出现在循环或者界面刷新这样的高频场景里,是完全不可控的
      • 这样一来这一类函数就全都有了性能隐患了
      • 高阶函数是Kotlin相当于Java一个很方便的特性,但却有这么一个性能隐患,这让人怎么放心用呢?这就是inline关键字出场的时候了
  • inline关键字不止可以内联自己内部的代码,还可以内联自己内部的内部的代码,什么意思呢?
    • 就是你的函数在被加了inline关键字之后
    • 编译器在编译时,不仅会把函数内联过来
    • 而且会把她内部的函数类型的参数,就是那些Lambda表达式也内联过来
inline fun hello(postAction: () -> Unit) {
  print("Hello!")
  postAction()  //做点后续工作
}

...
fun main(){
  hello{
    println("Bye!")
  }
}
//实际编译的代码
fun main(){
  println("Hello!")
  println("Bye!")
}
    • 换句话说,这个函数被编译器贴过来的时候,是完全展开铺平的
    • 经过这种优化,是不是就避免了函数类型的参数所造成的临时对象的创建呢?这样的话,是不是就不怕在循环或者界面刷新这样高频的场景里调用他们了
    • 这就是inline关键字的用处,高阶函数有他们天然的性能缺陷,我们用inline关键字让函数一内联的方式进行编译,来减少参数对象的创建,从而避免出现性能问题。所以inline是用来优化的吗?是的,但你不能无脑使用他,你需要确定他可以带来优化再去用它,否则有可能变成负优化
      • 换个角度想想,既然inline是优化,为什么Kotlin没有把她直接打开?而是把她做出一个选项?而且还是一个默认关闭的选项?就是因为他还真不一定是优化,加不加他需要我们自己去做判断
      • 那怎么去做判断什么时候用呢?
        • 如果你写的是高阶函数,会有函数类型的参数,加上inline就对了
        • 不过如果你们团队对于包大小有非常极致的追求,也可以选择酌情使用inline,比如对代码做严格要求,只有会被频繁调用的高阶函数才使用inline,这个可能在实施上会有点困难
        • 另外Kotlin的官方源码里面还有一个inline的另类用法,在函数里直接去调用Java的静态方法,用偷天换日的方式来去掉了这些Java的静态方法的前缀,让调用更简单
// MathJVM.kt部分源码
import java.lang.Math as nativeMath

@SinceKotlin("1.2")
@InlineOnly
public actual inline fun min(a: Int, b: Int): Int = nativeMath.min(a, b)

//Java
Math.min(a, b)

//Kotlin
min(a, b)
    • 这个很有必要跟大家提一下,这种用法不是inline被创造的初衷,也不是inline的核心意义,这属于一种相对偏门的另类用法
    • 不过这么用没什么问题,因为他的函数体简单,并不会造成字节码膨胀的问题,你如果有类似的场景,也可以这么用,讲到这,应该知道内联函数怎么用了吧?

函数参数不内联noinline

  • 说完inline,我们来说另一个关键字noinline
  • noinline的意思很直白,inline是内联,noinline就是不内联,不过她不是作用于函数的,而是作用于函数的参数
  • 对于一个标记了inline的内联函数,你可以对他的任何一个或者多个函数类型的参数添加noinline关键字
inline fun hello(preAction: () -> Unit, noinline postAction: () -> Unit) {
  preAction()  //做点前置工作
  println("Hello!)
  psotAction()  //做点后续工作
}

...
fun main(){
  hello({
    println(”Emm...“)
  }, {
    println("Bye!")
  })
}
  • 添加了之后,这参数就不会参与内联了
//  实际编译的代码
fun main() {
  println("Emm...")
  println("Hello!")
  ({
    println("Bye!")
  }).invoke()
}
  • 有什么用?
    • 为什么要关闭这种优化呢?
    • 首先我们要知道函数类型的参数,他本质上是个对象,我们可以把这个对象当做函数来调用,这也是最常见的用法
    • 但同时我们也可以把它当做对象来用,比如把她当做返回值
inline fun hello(preAction: () -> Unit, postAction: () -> Unit) {
  preAction()  //做点前置工作
  println("Hello!)
  psotAction()  //做点后续工作
  return postAction
}
    • 但当我们把函数进行内联的时候,他内部的这些参数就不再是对象了,因为他们会被编译器拿到调用处去展开
    • 也就是说,当你的函数被这样调用的时候
fun main(){
  hello({
    println(”Emm...“)
  }, {
    println("Bye!")
  })
}
  • 代码会被这样编译
//实际编译的代码
fun main(){
  println("Emm...")
  println("Hello!")
  println("Bye!")
  postAction
}
  • postAction是什么?你找谁呀?
    • 发现问题没有,当一个函数被内联的之后,他内部那些函数类型的参数就不再是对象了,因为他们的壳被脱掉了
  • 换句话说,对于编译之后的字节码来说,这个对象根本就不存在,一个不存在的对象你怎么用他?
  • 所以当你要把一个这样的参数当对象使用的时候,Android Studio会报错,告诉你没法编译,我如果真的需要用这个对象怎么办?,加上noinline我们就可以正常使用他了
inline fun hello(preAction: () -> Unit, noinline postAction: () -> Unit) {
  preAction()  //做点前置工作
  println("Hello!)
  psotAction()  //做点后续工作
  return postAction
}
fun main(){
  hello({
    println(”Emm...“)
  }, {
    println("Bye!")
  })
}
//实际编译的代码
fun main(){
  println("Emm...")
  println("Hello!")
  val postAction = ({
    println("Bye!")
  }).invoke()
  postAction
}
  • 所以noinline的作用的是什么?
    • 是用来局部的、指向性地关闭函数的内联优化,
    • 既然是优化,为什么要关闭?因为这种优化会导致函数里面的函数类型的参数无法被当做对象来使用。也就是说这种优化会对Kotlin的功能做出一定程度的收窄
    • 而当你需要这个功能的时候,就需要手动关闭优化了
    • 这也是inline默认关闭,需要手动开启的另外一个原因,他会收窄Kotlin的功能
  • 那么我们怎么判断什么时候用noinline呢?
    • 很简单,比inline还简单。你不用判断,Android Studio会告诉你
    • 当你在内联函数里对函数类型的参数进行了风骚操作,Android Studio拒绝编译的时候,你再加上noinline就可以了

crossinline

  • 这是一个很有意思的关键字,刚才讲的noinline是局部关闭内联优化对吧?
  • 而crossinline是局部加强内联优化
  • 看示例
    • 这里有一个内联函数和他的调用
inline fun hello(postAction: () -> Unit){
  println("Hello!")
  postAction()
}
fun main(){
  hello{
    println("Bye!")
  }
}
  • 假如我往Lambda表达式里面加一个return
fun main(){
  hello{
    println("Bye!")
    return
  }
}
  • 这个return会结束哪个函数的执行?
    • 是外层的hello还更外层的main?按照通常的规则,肯定是结束hello()的对吧?因为hello离她近呀,他所结束的可能是直接包裹住他的那个函数
    • 可是,大家想一想,这个hello()是个内联函数对不对?内联函数在编译优化之后会变成怎么样?会被铺平,而这个调用被铺平之后是这样
//  实际编译的代码
fun main(){
  println("Hello!")
  println("Bye!")
  return
}
    • 那你再看看return结束的是哪个函数?是外层的main对吧
    • 也就是说,对于内联函数,他的参数中的Lambda的return结束的不是这个内联函数,而是这个调用这个内联函数的更外层函数,是这个道理吧?道理是这个道理,但这就有问题了
    • 我一个return结束哪个函数,竟然要看这个函数是不是内联函数
    • 那岂不是我每次写这种都需要钻到原函数去看一看有没有inline关键字,然后才能知道我的代码会怎样执行?那这也太难了吧
  • 这种不一致性会给我们带来极大的困扰,因此Kotlin指定了一条规则
    • Lambda表达式里不允许使用return,除非这个Lambda是内联函数的参数
    • 这就简单了,Lambda里的return结束的不是直接的外层函数,而是外层再外层的函数,但只有内联函数的Lambda参数才可以使用return。
      • 注:Lambda表达式可以用return@label 的方式来显式指定返回的位置,但这不是今天讨论的内容
    • 只有既避免了歧义,又避免了需要反复查看每一个函数是不是内联函数的麻烦
    • 不过,如果我们把事情再变复杂一点
      • 这次我用runOnUiThrad把这个参数放在了主线程执行
inline fun hello(postAction: () -> Unit) {
  printn("Hello!")
  runOnUiThread {
    postAction()
  }
}

...
fun main() {
  hello {
    println("Bye!")
    return
  }
}
  • 这是一种很常见的操作,但,这就带来了一个麻烦:本来在调用处最后那行的return是要结束他外层再外层的main()函数的,但现在,因为他被放在了runOnUiThread()里,hello对他的调用就变成了间接调用
  • 所谓间接调用,直白点说就是他和外层hello()函数之间的关系被切断了,和hello的关系被切断,那就更够不着更外层的main()了,也就是这个间接调用导致Lambda里的return无法结束最外层的main()函数了,这就表示什么?
  • 当内联函数的Lambda参数,在函数内部是间接调用的时候,Lambda里面的return会无法按照预期的行为进行工作
  • 这就比较严重了,因为这造成了Kotlin这个语言的稳定性问题了,结果是不可预测的,这能行吗?那怎么办?Kotlin的选择依然是霸气一刀切
    • 内联函数里的函数类型的参数,不允许这种间接调用
inline fun hello(postAction: () -> Unit) {
  printn("Hello!")
  runOnUiThread {
    postAction()  //这里会报错
  }
}

...
fun main() {
  hello {
    println("Bye!")
    return
  }
}
  • 解决不了问题,我就解决提出问题的人
    • 那我如果真的有这种需求呢?如果我真的需要间接调用,使用crossinline
  • crossinline也是一个用在参数上的关键字
    • 当你给一个需要被间接调用的参数加上crossinline,就对她解除了这个限制
inline fun hello(crossinline postAction: () -> Unit) {
  printn("Hello!")
  runOnUiThread {
    postAction()  
  }
}

...
fun main() {
  hello {
    println("Bye!")
    return  //报错
  }
}
  • 从而就可以对她间接调用了,不过这又会导致前面说过的不一致问题,比如我在Lambda里加上一句return,他结束的是谁?
    • 是包着他的runOnUIThread(),还是依然是最外层的main()呢?
    • 对于这种不一致,Kotlin增加了一条额外规定:内联函数里被crossinline修饰的函数类型的参数,将不再享有Lambda表达式可以使用return的福利了,所以这个return并不会面临要结束谁的问题,而是直接就不允许这么写
  • 也就是说间接调用和Lambda的return你只能选择一个
  • 那什么时候使用crossinline?
    • 当你需要突破内联函数的不能间接调用参数的限制的时候
    • 但其实和noinline一样,你并不需要亲自去判断,只要看到Android Studio给你报错的时候加上就可以了

总结

  • inline可以让你用内联,也就是函数内容直插到调用处的方式来优化代码结构,从而减少函数类型的对象的创建
  • noinline是局部关闭这个优化,来摆脱inline带来的,不能把函数类型的参数当对象使用的限制
  • crossinline是局部加强这个优化,让内联函数里的函数类型的参数可以被间接调用