Kotlin⾼阶函数与内联函数

426 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第15天,点击查看活动详情

⾼阶函数

将一个函数作为参数或者返回值的函数就叫做⾼阶函数

举个例子,在我们项目中可能经常遇到字符串拼接,可能是把选择的多个用户的id用逗号分割传给服务端,也可能是把多个用户的名字用横线分割显示在界面上,遇到这种,如果我们每次都去写一次拼接方法,就会使代码显得很冗余。

在这个例子中,我们都是将一个List转化为一个String,至于分隔符是用逗号还是用横线,拼接的信息是用id还是name,这些都是变量,先给List定义一个扩展函数join(),用来转化,分隔符处理比较简单,可以作为一个参数由调用者传入,对于拼接信息,他不一定是当前对象,可能需要通过其他函数获取,如下:

fun getJoinInfoById(user: User) = user.id
fun getJoinInfoByName(user: User) = user.name

那么这个获取信息的函数怎么传给我们上面声明的函数join()呢,这时候就用到了高阶函数,也就是直接将getJoinInfoById或者getJoinInfoByName作为参数传个join

val users = listOf(User(1, "张三"), User(2, "李四"), User(3, "王五"))
val ids = users.join(separator = ",", getJoinInfo = ::getJoinInfoById)
val names = users.join(separator = "-", getJoinInfo = ::getJoinInfoByName)

传参数很方便,kotlin提供了::操作符,他的作用就是把一个方法当做一个参数,传到其他方法中使用。

到目前为止,我们的join()方法定义如下:

fun <T> List<T>.join(separator: CharSequence, getJoinInfo: ???): String {}

这里标记为???的地方就是接受函数参数的地方,我们如何接受这个函数类型的参数呢?首先明确下函数类型的概念,在kotlin中将函数的参数类型和返回值类型抽象出来就得到了函数类型,形如 (Int) -> String,它代表了有一个参数Int,返回值为String的函数类型。

在本例中,我们传入的参数是User,但是对于List更广泛点,传入泛型T,由于不同情况需要的拼接信息不一样,所以返回值为Any,于是函数类型可以定义为((T) -> Any)。join函数可以完善为:

fun <T> List<T>.join(separator: CharSequence, getJoinInfo: ((T) -> Any)): String {
    val buffer = StringBuffer()
    forEach {
        val info = getJoinInfo.invoke(it)
        if (buffer.isEmpty()) {
            buffer.append(info)
        } else {
            buffer.append(separator).append(info)
        }
    }
    return buffer.toString()
}

内联函数

虽然高阶函数使用起来很方便,但是实际使用起来有很多需要注意的地方。比如Kotlin是一个基于JVM的语言,他是怎么支持将函数作为参数的呢,难道是JVM给kotlin开了小灶吗,先反编译上面的class文件

@NotNull
  public static final <T> String join(@NotNull List<? extends T> $this$join, @NotNull CharSequence separator, @NotNull Function1<? super T, ? extends Object> getJoinInfo)
  {
    Intrinsics.checkNotNullParameter($this$join, "<this>");Intrinsics.checkNotNullParameter(separator, "separator");Intrinsics.checkNotNullParameter(getJoinInfo, "getJoinInfo");StringBuffer buffer = new StringBuffer();
    Iterable $this$forEach$iv = (Iterable)$this$join;int $i$f$forEach = 0;
    for (Object element$iv : $this$forEach$iv)
    {
      Object it = element$iv;int $i$a$-forEach-AAKt$join$1 = 0;Object info = getJoinInfo.invoke(it);
      if ((((CharSequence)buffer).length() == 0 ? 1 : 0) != 0) {
        buffer.append(info);
      } else {
        buffer.append(separator).append(info);
      }
    }
    $this$forEach$iv = buffer.toString();Intrinsics.checkNotNullExpressionValue($this$forEach$iv, "buffer.toString()");return $this$forEach$iv;
  }

调用处的反编译结果

public static final void main() {
   List users = CollectionsKt.listOf(new User[]{new User(1, "张三"), new User(2, "李四"), new User(3, "王五")});
   String ids = join(users, (CharSequence)",", (Function1)null.INSTANCE);
   String names = join(users, (CharSequence)"-", (Function1)null.INSTANCE);
   String var3 = "ids=" + ids + "   names=" + names;
   System.out.println(var3);
}

可以看到我们定义的((T) -> Any)函数类型不见了,取而代之的是 Function1<? super T, ? extends Object>,原来是将我们的函数类型转化为了Function1类型,而且调用方传入的函数参数也转化为Function1对象了。如果实在一个循环中调用高阶函数,那么创建这些对象的代价是非常大的,如何避免这种情况呢,kotlin引入了内联函数。

inline

为了避免上面创建Function实例的开销,只需要在函数前面用inline修饰符标记就可以了

inline fun <T> List<T>.join(separator: CharSequence, getJoinInfo: ((T) -> Any)): String {
    //省略实现
}

先打个预防针,后面反编译后的代码比较长,所以这里修改下调用处的代码,只保留一处调用就行了

val ids = users.join(separator = ",", getJoinInfo = ::getJoinInfoById)
println("ids=$ids")

接着反编译,看看调用的地方

CharSequence separator$iv = (CharSequence)",";
int $i$f$join = false;
StringBuffer buffer$iv = new StringBuffer();
Iterable $this$forEach$iv$iv = (Iterable)users;
int $i$f$forEach = false;
Iterator var8 = $this$forEach$iv$iv.iterator();
while(var8.hasNext()) {
   Object element$iv$iv = var8.next();
   int var11 = false;
   User p1 = (User)element$iv$iv;
   int var13 = false;
   Object info$iv = getJoinInfoById(p1);
   CharSequence var15 = (CharSequence)buffer$iv;
   if (var15.length() == 0) {
      buffer$iv.append(info$iv);
   } else {
      buffer$iv.append(separator$iv).append(info$iv);
   }
}

String var10000 = buffer$iv.toString();
String ids = var10000;
String var2 = "ids=" + ids;
System.out.println(var2);

可以看到这次没有生成Function1的对象了,而是把join()方法的代码直接内联到了调⽤处,这样就避免了性能的损耗。