Kotlin协程中高阶函数、内联函数以及对象类的invoke()函数理解

kotlin协程框架涉及很多关键字和语法糖,其中高阶函数、内联函数被多处使用

1、高阶函数

高阶函数其实就是一个函数(或者方法),将这个函数(或者方法)作为一个参数值传入到另一个函数里并且采用lambda表达式特性进行缩写,函数可以是有无传参或返回值,高阶函数也是

例如定义一个打印log高阶函数

例 1

private fun logd(msg: () -> String){
   Log.d("LOG", ">>> ${msg()}" )
}
复制代码

其中参数 msg: () -> String 为声明一个高阶函数,msg为这个高阶函数的命名(可以随意);()为这个高阶函数的传参声明,当前为无参,有参数可以写成(a: String, b: Int)或者(String, Int)-> String为这个高阶函数的返回值,箭头指向表示高阶函数的返回对象类型,当前为返回字符串类型,无返回值可以写成 -> Unit(Unit源码解释: The type with only one value: the Unit object. This type corresponds to the void type in Java. 意思是类似java中的void

打印log高阶函数调用:

logd { "测试" }
复制代码

调用高阶函数采用lambda表达式进行缩写,kotlin中有约定:当函数只有一个传参并且参数是高阶函数时可以省略()进行缩写;当函数不止一个传参但是最后一个传参是高阶函数时,可以在()外指定

例 2

//定义
private fun logd(tag: String, msg: () -> String){
   Log.d(tag, ">>> ${msg()}" )
}

//调用
logd("LOG"){
  "测试"
}
复制代码

总结

  • 将函数(或者方法)作为传参并不稀奇, java中可以传有返回值的函数作为参数,但是java会优先执行这个传参函数,而kotlin中的高阶函数不同,这样传参并不会立刻执行,只有调用了才会执行传的函数,这样可以做很多事,比如将这个函数挂载到子线程执行等等,kotlin协程框架就是按照这个语言特性设计的,kotlin协程框架可以说是语言特性造就的一个框架

  • 缺陷就是高阶函数会被编译成匿名接口类,每调用一次会创建一次,类的开销很大,需要采用内联函数来解决(生成匿名类的举例在内联函数中会详细介绍)

协程框架很多地方使用了高阶函数,其中有:launch函数与async函数:

//launch函数
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    ···
}

//async函数
public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    ···
}
复制代码

将函数挂载到一个协程里执行

2、内联函数
通过inline关键字修饰的函数方法称之为内联函数

复制代码

内联函数的作用

(1)、防止高阶函数反编译为匿名类,减少类创建开销

例 3

class InlineTest {

    fun test(){
        logd {
            "测试"
        }

        logi {
            "测试"
        }
    }


    private fun logd(msg: () -> String){
        Log.d("LOG", ">>> ${msg()}" )
    }

    inline fun logi(msg: () -> String){
        Log.i("LOG", ">>> ${msg()}" )
    }
}
复制代码

反编译结果:

@Metadata(
   mv = {1, 1, 16},
   bv = {1, 0, 3},
   k = 1,
   d1 = {"\u0000\u001e\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\u0010\u000e\n\u0002\b\u0003\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0016\u0010\u0003\u001a\u00020\u00042\f\u0010\u0005\u001a\b\u0012\u0004\u0012\u00020\u00070\u0006H\u0002J\u0017\u0010\b\u001a\u00020\u00042\f\u0010\u0005\u001a\b\u0012\u0004\u0012\u00020\u00070\u0006H\u0086\bJ\u0006\u0010\t\u001a\u00020\u0004¨\u0006\n"},
   d2 = {"Lcom/wxk/coroutines/InlineTest;", "", "()V", "logd", "", "msg", "Lkotlin/Function0;", "", "logi", "test", "app"}
)
public final class InlineTest {
   public final void test() {
      this.logd((Function0)null.INSTANCE);
      int $i$f$logi = false;
      StringBuilder var5 = (new StringBuilder()).append(">>> ");
      String var4 = "LOG";
      int var3 = false;
      String var6 = "测试";
      Log.i(var4, var5.append(var6).toString());
   }

   private final void logd(Function0 msg) {
      Log.d("LOG", ">>> " + (String)msg.invoke());
   }

   public final void logi(@NotNull Function0 msg) {
      int $i$f$logi = 0;
      Intrinsics.checkParameterIsNotNull(msg, "msg");
      Log.i("LOG", ">>> " + (String)msg.invoke());
   }
}
复制代码

匿名接口Function0:

/** A function that takes 0 arguments. */
public interface Function0<out R> : Function<R> {
    /** Invokes the function. */
    public operator fun invoke(): R
}
复制代码

反编译的代码比较明显,logd()方法没有被inline关键字修饰,在反编译后会有创建一个匿名接口类Function0;被inline关键字修饰的logi()方法则是相当于拷贝了一份代码到test()方法中,没有而外的创建匿名类

(2)、杜绝频繁调用相同方法造成方法进栈出栈所带来的性能损耗

例 4

class InlineTest {

    fun test(){

        for (i in 1..3){
            logd()
        }

        for (i in 1..3){
            logi()
        }

    }

    private fun logd(){
        Log.d("LOG", ">>> logd" )
    }

    inline fun logi(){
        Log.i("LOG", ">>> logi" )
    }
}
复制代码

反编译结果:

@Metadata(
   mv = {1, 1, 16},
   bv = {1, 0, 3},
   k = 1,
   d1 = {"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0002\b\u0003\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\b\u0010\u0003\u001a\u00020\u0004H\u0002J\t\u0010\u0005\u001a\u00020\u0004H\u0086\bJ\u0006\u0010\u0006\u001a\u00020\u0004¨\u0006\u0007"},
   d2 = {"Lcom/wxk/coroutines/InlineTest;", "", "()V", "logd", "", "logi", "test", "app"}
)
public final class InlineTest {
   public final void test() {
      int var1 = 1;

      byte var2;
      for(var2 = 3; var1 <= var2; ++var1) {
         this.logd();
      }

      var1 = 1;

      for(var2 = 3; var1 <= var2; ++var1) {
         int $i$f$logi = false;
         Log.i("LOG", ">>> logi");
      }

   }

   private final void logd() {
      Log.d("LOG", ">>> logd");
   }

   public final void logi() {
      int $i$f$logi = 0;
      Log.i("LOG", ">>> logi");
   }
}
复制代码

通过反编译结果可以看出, logd()方法没有被inline关键字修饰,在循环调用时会逐个调用logd()方法,会造成方法大量进栈出栈操作;被inline关键字修饰的logi()方法则是相当于拷贝了一份代码到循环体中,没有频繁调用logi()方法

(3)、泛型对象的简易传输与泛型实体类型获取(不需要强转操作)

例 5

class InlineTest {
    fun test(){
        val json = "{\"age\":18,\"name\":\"kk\"}"
        val generic = GenericTest().resolve<Generic>(json)
        Log.d("LogUtils", "age == ${generic.age}")
        Log.d("LogUtils", "name == ${generic.name}")
    }
}

/**
 * 定义内联函数
 */
class GenericTest{
    inline fun <reified T> resolve(json: String): T {
        return resolve(T::class.java, json)
    }

    fun <T> resolve(clazz: Class<T>, json: String) : T{
        return Gson().fromJson(json, clazz) as T
    }
}

/**
 * 定义一个实体对象
 */
data class Generic(
    val age: Int,
    val name: String
)
复制代码

运行结果:

D/LogUtils: age == 18
D/LogUtils: name == kk
复制代码

通过 例5GenericTest对象的内联操作, 泛型T得到的是具体实体对象Generic,不需要通过强转得到, GenericTest对象也无需引用Generic

3、kotlin通用的invoke()函数

invoke()方法是kotlin对象类中默认持有的方法,可以通过operator关键字重载invoke()方法

例 6

enum class OperatorTest {

    TEST;

    operator fun invoke(data: String){
        Log.d("LogUtils","data : $data")
    }
}

fun execute(){
    val start = OperatorTest.TEST
    //原始调用方式
    start.invoke("测试1")
    //简化调用方式
    start("测试2")
}
复制代码

运行结果:

D/LogUtils: data : 测试1
D/LogUtils: data : 测试2
复制代码

kotlin类默认含有invoke()方法,并且可以通过operator关键字重载,可以采用原始调用方式:class.invoke(···);kotlin允许简易调用:class()

参考

1、Kotlin 中的 lambda,这一篇就够了

2、关键字与操作符

分类:
Android
标签: